In Dart, what element types does inference give `var a = [];`, `var b = [1, 2.5];` and `List<int> c = [];`, and why?
answer
- context first, then the elements
- downward from the declared type
- upward from the elements
- int and double meet at num
- nothing to go on means dynamic
basics
~10 sa 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 sDart 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 linesvoid 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
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.
Explain downward versus upward inference and how a generic call like map combines them, with the closure's return type deciding the type argument.
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.
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