skip to content

Covariance & Inference

Dart treats List<int> as a List<num>, so a bad write compiles and throws at run time, and inference fills omitted type arguments. Interviewers ask why it stays sound and when inference picks dynamic.

part ofDartoverview, primer and where to startread it →
on this pageshow

explore

questions

4

In Dart, what element types does inference give `var a = [];`, `var b = [1, 2.5];` and `List<int> c = [];`, and why?

level: juniorimportance: should knowfreq 48%

answer

  1. context first, then the elements
  2. downward from the declared type
  3. upward from the elements
  4. int and double meet at num
  5. nothing to go on means dynamic

basics

~10 s

a is List<dynamic> because nothing constrains it, b is List<num> because upward inference takes the upper bound of int and double, and c is List<int> because the declared type flows down into the literal.

solid answer

~40 s

Dart fills omitted type arguments from two directions. **Downward** inference pushes the context type into the expression: in `List<int> c = []` the literal becomes `<int>[]`. **Upward** inference computes the type from the parts when there is no context: `[1, 2.5]` gets the upper bound of `int` and `double`, which is `num`; `[1, 'a']` would be `List<Object>` and `[1, null]` `List<int?>`. With neither, as in `var a = []`, inference has nothing to go on and uses `dynamic`, so `a` is a `List<dynamic>` that accepts anything and makes every element read a dynamic access. Inference looks only at the expression and its context, never at later `add` calls, so write `<int>[]` or give the variable a type when a literal starts empty.

code

dart · 12 lines
dart
void main() {
  var a = []; // List<dynamic>: no context, no elements
  var b = [1, 2.5]; // List<num>: upper bound of int and double
  List<int> c = []; // List<int>: the declared type flows down
  var d = [1, 'a']; // List<Object>
  var e = [1, null]; // List<int?>

  a.add('anything'); // compiles: a List<dynamic> takes any value
  // c.add('x'); // compile error: a String is not an int
  var halves = b.map((x) => x / 2); // Iterable<double>
  print(halves.first + d.length + e.length + c.length); // 4.5
}

go deeper

for a junior

Know the three answers by heart: an empty untyped literal is List<dynamic>, mixed int and double gives num, and a declared type flows into the literal.

for a middle

Explain downward versus upward inference and how a generic call like map combines them, with the closure's return type deciding the type argument.

for a senior

Spot where dynamic sneaks in through empty literals and too-wide upper bounds, and explain why the resulting List<dynamic> fails later as a TypeError or NoSuchMethodError.

for a principal

Decide how much a codebase should rely on inference versus explicit type arguments, and back the rule with analyzer strictness rather than review comments.

## Two directions of information A **type argument** is the `int` in `List<int>`. When you leave it out of a collection literal or a generic call, Dart's **type inference** fills it in at compile time. The analyzer uses two sources of information: - **Downward inference** — the **context type** flows *into* the expression. A declared variable type, a parameter type or a return type are all contexts. In `List<int> c = [];` the context `List<int>` makes the literal `<int>[]`. - **Upward inference** — when the context does not decide, the type is computed *from* the parts. For a list literal that means the **upper bound** of the element types: the most specific type that all of them share. When both are missing — no context and no elements — there is nothing to infer from, and Dart uses **`dynamic`**. ## What each literal gets | Declaration | Inferred literal type | Source of the answer | |---|---|---| | `var a = [];` | `List<dynamic>` | no context, no elements | | `var b = [1, 2.5];` | `List<num>` | upward: upper bound of `int` and `double` | | `List<int> c = [];` | `List<int>` | downward: the declared type | | `var d = [1, 'a'];` | `List<Object>` | upward: `int` and `String` meet only at `Object` | | `var e = [1, null];` | `List<int?>` | upward: `int` plus `Null` is `int?` | | `var m = {};` | `Map<dynamic, dynamic>` | an empty `{}` is a map literal with no information | | `var args = {'a': 'hello', 'b': 42};` | `Map<String, Object>` | upward, per key and per value | The type is fixed at the declaration. Inference does **not** look ahead at later statements: `var a = []; a.add(1);` does not make `a` a `List<int>`, it stays `List<dynamic>` for its whole life, and because the literal's type argument is reified, the runtime object is a `List<dynamic>` too. ## Generic method calls use both directions Inference is not limited to literals. For a generic method call, Dart combines downward information from the context with upward information from the arguments: ```dart var doubles = [3.0]; // List<double> var ints = doubles.map((x) => x.toInt()); // Iterable<int> ``` Here `x` gets the type `double` downward from `map`'s parameter type, the closure's return type `int` flows upward, and that return type becomes `map`'s type argument, giving `Iterable<int>`. If inference picks something you did not want, you can always write the type argument yourself: `doubles.map<num>(...)`. ## Why `dynamic` is the expensive answer A `List<dynamic>` compiles with almost anything, which is exactly the problem: 1. It accepts elements of every type, so a wrong value goes in silently. 2. Every element read is a `dynamic` value, so a misspelt member such as `a.first.lenght` compiles and fails with a `NoSuchMethodError` at run time. 3. It cannot become a typed list later. `List<int> ids = a;` is a compile error (a downcast from a non-`dynamic` type), and `a as List<int>` compiles but throws a `TypeError`, because the runtime object really is a `List<dynamic>` even when every element is an `int`. A `List<num>` from upward inference is less surprising but can still be too wide: if you meant a list of doubles, `[1, 2.5]` gives you `num` elements and you lose `double`-only members. Writing `<double>[1, 2.5]` fixes it. ## Where to write the type argument yourself Effective Dart turns this into two complementary rules: - **Write type arguments on generic invocations that aren't inferred.** `var playerScores = {};` and `final events = StreamController();` both end up with `dynamic`; write `<String, int>{}` and `StreamController<Event>()`. - **Don't write type arguments that are inferred.** `final Completer<String> response = Completer();` already gives the constructor call its `<String>` through downward inference, and `var items = Future.value([1, 2, 3]);` infers `Future<List<int>>` upward from the elements, so repeating the types adds noise, not safety. For a field or top-level variable, putting the type on the declaration instead of on the literal is equally good: the declaration becomes the context, and the literal's type argument *is* then inferred. ## Practical rules - Give an **empty** literal a type argument (`<String>[]`) or a typed declaration. - Let inference work when the elements or the context already say it all; do not annotate what is obvious. - Watch **mixed** literals: they infer the upper bound, which may be `Object`. - The analyzer's `strict-inference` mode reports the places where inference falls back to `dynamic`, such as an untyped empty literal; configuring it belongs with the analyzer setup.

  • Why does `var a = []; a.add(1); final ids = a as List<int>;` throw although `a` holds only ints?
    `a` was inferred as `List<dynamic>` at its declaration, and the runtime object carries that type argument for good. The cast checks the object's type, not its elements, and `List<dynamic>` is not a subtype of `List<int>`, so it throws a `TypeError`. Use `List<int>.from(a)` or `a.cast<int>()`, or better, create it as `<int>[]`.
  • When would you write an explicit type argument on a non-empty literal such as `<double>[1, 2.5]`?
    When the upper bound inference computes is wider than you intend. `[1, 2.5]` infers `List<num>`, so you lose `double`-only members and can later add an `int`. `<double>[1, 2.5]` makes the literal a `List<double>`; the integer literal `1` is then read as the double `1.0` because its context type is `double`.

saying these in an interview costs you the question

  • Thinks var a = [] becomes List<int> after a.add(1)
  • Says a mixed list of int and double infers List<double>
  • Believes an empty literal without context is a compile error
  • Assumes List<dynamic> and List<int> are interchangeable at run time
  • Claims inference picks Object when it has no information
open as a page

In Dart, why does passing a List<Cat> where a List<Animal> is expected compile, and what happens when the callee then adds a Dog?

level: middleimportance: should knowfreq 42%

basics

~10 s

Dart generic classes are covariant, so List<Cat> is a subtype of List<Animal> and the call compiles. The list keeps its reified List<Cat> type, so add(Dog()) fails a runtime parameter check and throws a TypeError.

open as a page

A Dart model assigns a decoded JSON array to a List<String> field; it compiles but throws at run time. Why, and how do you stop dynamic type arguments leaking?

level: seniorimportance: should knowfreq 36%

basics

~20 s

jsonDecode returns dynamic, so the assignment is an unchecked-at-compile-time downcast, and the decoded array is a List<dynamic> at run time. A List<dynamic> is not a List<String>, so it throws a TypeError even when every element is a String.

open as a page

In Dart, what does the covariant keyword on a parameter do, and why do Flutter overrides like CustomPainter.shouldRepaint rely on it?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

covariant lets an override narrow a parameter to a subtype, which is otherwise an invalid override. The analyzer accepts it and Dart checks the argument at run time instead, so Flutter subclasses can write shouldRepaint(MyPainter oldDelegate).

open as a page