skip to content

A legacy Dart codebase leans on dynamic values and raw generic types; how do you roll out strict-inference, strict-casts and the no_raw_types lint without stalling the team?

level: seniorimportance: should knowfreq 24%

answer

  1. stricter than the type system requires
  2. implicit casts from dynamic
  3. inference falling back to dynamic
  4. 3.13 turns two modes into lints
  5. measure, ratchet, then gate

basics

~20 s

Measure first, then tighten in stages: enable one check at a time, fix module by module starting at dynamic boundaries such as jsonDecode, keep new code clean, and make it blocking once findings reach zero. On Dart 3.13 prefer the no_dynamic_casts and no_raw_types lints.

solid answer

~40 s

The three checks go beyond what Dart's sound type system requires, and all default to off. `strict-casts` reports implicit downcasts from `dynamic`, such as passing `jsonDecode(text)` to a `List<String>` parameter. `strict-inference` reports places where inference falls back to `dynamic`, such as an untyped empty `{}` or an untyped parameter. `strict-raw-types` reports generic types written without type arguments, like `List`. Dart 3.13 added `no_dynamic_casts` and `no_raw_types` lint rules that replace the `strict-casts` and `strict-raw-types` options; being lints, they take `analyzer: errors:` severities and ignore comments. Rollout: count findings per check with `dart analyze`, turn on one check at a time, fix hot and boundary code first by parsing JSON into typed models, keep new files clean, then raise the rule to `error` and gate CI. Avoid silencing it with `as dynamic` casts.

code

dart · 12 lines
dart
import 'dart:convert';

void printAll(List<String> lines) => lines.forEach(print);

void load(String text) {
  // Implicit downcast from dynamic: reported by strict-casts / no_dynamic_casts.
  // printAll(jsonDecode(text));

  // Typed at the boundary instead:
  final decoded = jsonDecode(text) as List<Object?>;
  printAll([for (final item in decoded) item as String]);
}

go deeper

for a junior

Recall what dynamic and a raw type such as List without type arguments are, and that the analyzer can be told to flag them.

for a middle

Explain what each check reports with one example, and that Dart 3.13 made two of them lint rules.

for a senior

Lay out a measured, staged rollout: fix data boundaries first, keep new code clean, quarantine old files visibly, and gate at zero.

for a principal

Decide whether the migration pays off for a codebase, weighing run-time type errors in production against the team time the rollout consumes.

## What the strict checks add Dart's type system is **sound**, but it still lets `dynamic` flow silently: a `dynamic` value can be assigned to a typed variable, which inserts a run-time check, and inference can quietly pick `dynamic` when it has nothing better. Legacy code, especially code that predates null safety or parses JSON by hand, is full of both. The analyzer offers opt-in checks that surface these spots at analysis time. All default to `false`. | Check | Reports | Example | |---|---|---| | `strict-casts` | implicit downcasts from `dynamic` | `foo(jsonDecode(text))` where `foo` takes `List<String>` | | `strict-inference` | inference falling back to `dynamic` | `final lines = {};` with no type arguments | | `strict-raw-types` | generic types written without type arguments | `List numbers = [1, 2, 3];` | `strict-inference` reports diagnostics such as `inference_failure_on_collection_literal`, `inference_failure_on_untyped_parameter` and `inference_failure_on_function_return_type`. `strict-raw-types` reports `strict_raw_type`. `strict-casts` turns the implicit cast into an `argument_type_not_assignable`-style error that you fix with an explicit `as`, a type check, or typed parsing. ## The Dart 3.13 change Dart 3.13's changelog introduces two **lint rules** that replace two of the language modes: - **`no_dynamic_casts`** replaces the `strict-casts` option; - **`no_raw_types`** replaces the `strict-raw-types` option. `strict-inference` has no lint replacement in that release and stays under `analyzer: language:`. The lints matter for a rollout because they behave like any other rule: you enable them under `linter: rules:`, set their severity under `analyzer: errors:`, and suppress individual legacy spots with `// ignore:` or `// ignore_for_file:` comments. That gives you the knobs a gradual migration needs. ```yaml analyzer: language: strict-inference: true errors: no_dynamic_casts: info # visible, not yet blocking no_raw_types: warning # blocking by default in dart analyze linter: rules: no_dynamic_casts: true no_raw_types: true ``` ## A staged rollout 1. **Measure.** On a branch, enable one check and run `dart analyze`, counting findings per package and directory. The counts tell you where `dynamic` really lives, usually JSON parsing, platform-channel results and old collection code. 2. **Pick an order.** Raw types are often the cheapest (mostly adding type arguments); dynamic casts at data boundaries are the most valuable, since they are where run-time `TypeError`s come from. 3. **Fix at the boundary.** Replace `Map<String, dynamic>` passed deep into the app with typed models parsed once at the edge. A single typed `fromJson` removes dozens of downstream casts. 4. **Protect new code.** Enable the check for everyone at a non-blocking severity so new findings are visible in review. In a repository with several packages, a package's own `analysis_options.yaml` can turn a check on for cleaned-up packages first. 5. **Quarantine the rest.** For files nobody is touching this quarter, a file-level ignore of the lint is acceptable if it is tracked; turn on `unnecessary_ignore` so the ignores disappear as files are fixed. 6. **Gate.** When a package reaches zero, raise the rule to `error` (or pass `--fatal-infos`) so it cannot regress. ## What not to do - **Do not mass-insert `as` casts** to make the count drop. An explicit `as List<String>` still fails at run time on bad data; it just moves the failure from invisible to visible without fixing it. - **Do not flip every check to blocking on day one.** A red build nobody can fix trains the team to bypass the gate. - **Do not exclude the legacy folders** with `analyzer: exclude:`; that hides real errors in them as well. ## Related rules `avoid_dynamic_calls` flags method calls and property access on `dynamic` targets, which complements the cast check: one stops `dynamic` flowing into typed code, the other stops you calling into it blindly. ## Tracking progress and selling the change A migration like this competes with feature work, so it needs to be visible and cheap to continue: - **Publish the counts.** A small script that runs `dart analyze --format=json` and counts findings per rule and per package gives a number that should only go down. - **Attach fixes to touched files.** A common rule is "leave every file you edit strict-clean", which spreads the cost across normal work instead of a big-bang sprint. - **Show the payoff.** Each dynamic cast removed at a JSON boundary is a run-time `TypeError` that can no longer reach users; pointing at a real crash report of that shape is the strongest argument for the work. - **Know when to stop.** Generated code and third-party wrappers may never be worth converting; exclude or quarantine them explicitly rather than letting them hold the gate open.

  • Why does an explicit as cast not fully fix a strict-casts finding?
    It makes the check visible but keeps it at run time: if the JSON holds a number where a string was expected, `as String` still throws a `TypeError`. The real fix is validating or parsing the data into a typed model at the boundary, where a bad payload can be handled rather than crashing deep in the app.
  • What does moving strict-raw-types to the no_raw_types lint in Dart 3.13 buy a migration?
    Lint controls: the rule can be given an `analyzer: errors:` severity, enabled per package, and suppressed per line or per file with ignore comments. That makes a staged rollout possible, keeping legacy files quiet while new code must comply.

saying these in an interview costs you the question

  • Silences strict-casts findings by adding as casts everywhere
  • Turns on every strict check as blocking in one commit on a large codebase
  • Believes strict-inference changes what the program does at run time
  • Thinks the strict modes are on by default in Dart 3
  • Excludes legacy directories from analysis instead of tracking ignores