skip to content

In Dart, why does final adminTiles = [ListTile(title: Text('Users'))] reject a later adminTiles.add(const Divider()), and when should a literal carry <Widget>?

level: middleimportance: should knowfreq 38%

answer

  1. inference from context, then elements
  2. no context: element types decide
  3. empty [] alone is List<dynamic>
  4. children: supplies List<Widget>
  5. <Widget>[] or a declared type

basics

~20 s

Without a context type, a literal infers its element type from its own elements, so that list is List<ListTile> and a Divider cannot be added; write <Widget>[...] or declare List<Widget> when the list must hold other widget types or starts empty.

solid answer

~40 s

Dart infers a collection literal's type argument from **downward** context first and from its **elements** otherwise. In `Column(children: [...])` the parameter type `List<Widget>` is the context, so no annotation is needed. In `final adminTiles = [ListTile(...)]` there is no context, so the elements decide: `List<ListTile>`, and `add(const Divider())` is a compile error because `Divider` is not a `ListTile`. Local variable inference uses only the initializer, never later calls. Fix it with a typed literal `<Widget>[...]`, a declared type `final List<Widget> adminTiles = [...]`, or a helper with a `List<Widget>` return type. An empty `[]` with no context becomes `List<dynamic>`, which silently loses type checking, so empty literals should always carry a type argument such as `<String>[]`.

code

dart · 12 lines
dart
import 'package:flutter/material.dart';

List<Widget> adminEntries({required bool isAdmin}) {
  final entries = <Widget>[
    const ListTile(title: Text('Users')),
  ];
  if (isAdmin) entries.add(const Divider());
  return entries;
}

// Without <Widget>, entries would be List<ListTile>
// and entries.add(const Divider()) would not compile.

go deeper

for a junior

Recall that an empty literal needs a type such as <String>[] and that children: already provides List<Widget>.

for a middle

Explain downward versus upward inference, why local inference uses only the initializer, and the three ways to give an extracted list the right type.

for a senior

Catch extraction refactors that silently narrow a list's type, and enforce typed empty literals so dynamic does not leak into widget code.

for a principal

Set analyzer strictness, such as strict-inference in analysis_options.yaml, so untyped empty collections and implicit dynamic surface in review across the codebase.

## How a literal gets its type argument Every Dart list, set and map literal has a type argument, written or inferred: `[1, 2]` is really `<int>[1, 2]`. When you do not write it, **type inference** fills it in from two directions: 1. **Downward**, from the context the literal appears in: a declared variable type, a parameter type, a return type. 2. **Upward**, from the literal's own elements, when there is no context. With no context and mixed element types, the result is their **upper bound**: `var args = {'argA': 'hello', 'argB': 42}` is a `Map<String, Object>`, because `Object` is the closest type that both `String` and `int` share. ## Why the Divider is rejected ```dart final adminTiles = [ListTile(title: Text('Users'))]; adminTiles.add(const Divider()); // compile error ``` - `adminTiles` has no declared type, so it takes the literal's type. - The literal has no context, so its elements decide: one `ListTile` gives `List<ListTile>`. - **Local variable inference uses only the initializer**; the later `add` call does not widen the type. - `List<ListTile>.add` takes a `ListTile`, and a `Divider` is not a `ListTile`, so the analyzer reports an argument type error. The fixes all give the literal the type you actually mean: | Fix | Code | |---|---| | Typed literal | `final adminTiles = <Widget>[ListTile(...)];` | | Declared variable type | `final List<Widget> adminTiles = [ListTile(...)];` | | Helper return type | `List<Widget> adminTiles() => [ListTile(...)];` | ## When no annotation is needed Most Flutter children lists are written inline: `Column(children: [Text('a'), Icon(Icons.add)])`. The `children` parameter is declared `List<Widget>`, so that context flows down and the literal is `List<Widget>` even though its elements are a `Text` and an `Icon`. The same applies to elements added by `if`, `for` and spread: every branch just has to be assignable to the context element type. This is why the problem appears when code is **extracted**: moving the literal out of `children:` into a local variable removes its context, and its type silently narrows to whatever its current elements happen to share. ## Empty literals An empty literal has no elements to infer from: - `List<int> ids = [];` is fine: the declared type is the context, as if you wrote `<int>[]`. - `var ids = [];` has neither, so it becomes **`List<dynamic>`**. Calls on its elements are not checked, and passing it where a `List<int>` is expected is an error. Effective Dart's collection-literal guidance writes empty literals with the type argument: `var points = <Point>[];`, `var addresses = <String, Address>{};`, `var counts = <int>{};`. ## Sets, maps and control-flow elements The same inference applies to every literal kind and to elements produced by control flow: - `<String>{}` is an empty `Set<String>`; `<String, int>{}` is an empty `Map<String, int>`. - `[if (isAdmin) const Divider() else const SizedBox.shrink()]` with no context infers from **both** branches, so the list gets their shared upper bound. - A spread contributes its source's element type: `[...tiles]` where `tiles` is `List<ListTile>` is a `List<ListTile>`. - With a context type, every branch and every spread only has to be assignable to the context element type. So a list that looked fine while written inline can change type the moment an `else` branch is added or removed, if it has no context. ## Guidelines interviewers expect - Let the context infer the type when the literal is written inline in a widget argument. - Add a type argument or a declared type when the literal is **empty**, **extracted** into a variable, or must later hold **other subtypes**. - Remember that `final` fixes the variable, not the list's contents; the type argument is what limits which elements it accepts. - Prefer a typed literal to casting after the fact. Assigning a narrowly typed list to a wider variable, such as a `List<ListTile>` to a `List<Widget>`, compiles, and adding a `Divider` through the wider variable then fails at runtime; that is Dart's generic covariance, a separate topic.

  • Why does Column(children: [Text('a'), Icon(Icons.add)]) need no type argument?
    The `children` parameter is declared `List<Widget>`, and that context flows down into the literal, so it is inferred as `List<Widget>`. Each element only has to be assignable to `Widget`. The annotation matters only when the literal loses that context, for example when extracted into a local variable.
  • What type does var names = []; get, and why is that a problem?
    `List<dynamic>`, because there is neither a context type nor any element to infer from. Member calls on its elements are not statically checked, and passing it to a `List<String>` parameter is an error. Write `<String>[]` or declare `List<String> names = [];`.

saying these in an interview costs you the question

  • Dart infers a local list's type from the add calls that follow it
  • A list literal in a Flutter children argument always needs <Widget>
  • var items = [] gives a List<Object?> that is still type-checked
  • final on the variable is what stops a Divider being added
  • Mixed element types without context always infer dynamic