skip to content

In Dart, what does it mean that generics are reified, and what can code do with a type parameter like T at run time?

level: middleimportance: must knowfreq 50%

answer

  1. type arguments survive compilation
  2. is List<String> actually works
  3. x is T inside the generic
  4. T as a Type value
  5. no T() and no static calls

basics

~20 s

Reified means every generic object keeps its type arguments at run time, so is List<String> checks, x is T tests and <T>[] literals work. T still cannot be constructed or used to call static members.

solid answer

~40 s

Dart keeps type arguments at run time: a `<String>[]` knows it is a `List<String>`, so `names is List<String>` is `true` and `names is List<int>` is `false`. Inside generic code `T` is a real value you can use: `value is T`, `value as T`, `<T>[]` to create a correctly typed list, and `T` itself as a `Type` for logging or comparison. `Iterable.whereType<T>()` relies on exactly this. What you still cannot do is call a constructor or static member through `T`, so `T()` and `T.fromJson(...)` do not compile; generic code takes a factory function instead. Flutter leans on reification too: `context.dependOnInheritedWidgetOfExactType<T>()` finds an ancestor by the runtime type `T`. Languages that erase type arguments, such as Java, cannot test `is List<String>` at run time.

code

dart · 23 lines
dart
class Page<T> {
  Page(this.items);
  final List<T> items;

  bool accepts(Object? value) => value is T;
  Type get itemType => T;
  List<T> emptyLike() => <T>[];
}

void main() {
  final names = <String>['ada'];
  print(names is List<String>); // true
  print(names is List<int>); // false

  final page = Page<int>([1, 2]);
  print(page.accepts(3)); // true
  print(page.accepts('3')); // false
  print(page.itemType == int); // true
  print(page.emptyLike() is List<int>); // true

  final mixed = <Object>[1, 'two', 3];
  print(mixed.whereType<int>().toList()); // [1, 3]
}

go deeper

for a junior

Recall that Dart keeps type arguments at run time, so is List<String> works.

for a middle

Explain what you can do with T at run time, is T, as T, <T>[], and why T() and static calls through T are not allowed.

for a senior

Use reification deliberately, for type-keyed lookups and typed collection creation, while passing factories for construction and parsing.

for a principal

Weigh APIs keyed on runtime types against explicit registration, considering minified web builds and the readability of type-driven lookups.

## Reified versus erased A **generic type** such as `List<E>` is instantiated with a **type argument**, as in `List<String>`. A language can either keep that argument in the running program (**reified generics**) or check it at compile time and then drop it (**erasure**). Dart reifies: the dart.dev language tour says generic types *carry their type information around at runtime*. Java is the usual contrast; it erases type arguments, so a running Java program can ask whether an object is a `List`, but not whether it is a `List<String>`. ## What reification lets Dart code do | Operation | Example | Works because | |---|---|---| | test a generic type | `names is List<String>` | the list object records `String` | | test against a type parameter | `value is T` inside `Page<T>` | `T` is bound to a real type at run time | | cast to a type parameter | `value as T` | the cast is actually checked | | create a typed collection | `<T>[]`, `<String, T>{}` | the new object gets the real `T` | | read the type | `T`, `runtimeType` | `T` is available as a `Type` value | | filter by type | `items.whereType<T>()` | implemented with an `is T` test | A consequence worth remembering: a list's **runtime** type argument is the one it was **created** with, not the one of the variable holding it. A `List<Object>` variable can refer to a `List<String>` object, and `is List<String>` on it returns `true`. ## What reification does not give you Having `T` at run time does not make it a class reference: 1. **No constructor calls.** `T()` does not compile; the type parameter may not even have an unnamed constructor, or may be abstract. 2. **No static members.** `T.fromJson(json)` and `T.values` do not compile; Dart has no static access through type variables. 3. **No reflection on members.** Knowing `T` does not let you list its fields without `dart:mirrors`, which Flutter does not support. The standard Dart answer is to **pass behaviour in**: a `T Function(Map<String, dynamic>) fromJson` parameter, a factory closure, or a list of values. ## Where Flutter depends on it - `context.dependOnInheritedWidgetOfExactType<T>()` and `findAncestorStateOfType<T>()` walk the element tree comparing each ancestor against the runtime type `T`. - Packages that look things up by type, such as dependency-injection containers keyed by `T`, store and compare `Type` values at run time. - `whereType<T>()` filters mixed lists safely. None of these would work under erasure without passing an explicit type token. ## Static type versus runtime type argument Two different types are in play for every generic value: 1. The **static type** is what the compiler knows from declarations, such as the variable `List<Object> items`. 2. The **runtime type argument** is what the object was created with, such as `<String>['a']`. Assignments, method calls and inference work with the static type. `is`, `as`, `whereType` and type-keyed lookups work with the runtime one. Most surprising generic behaviour in Dart, such as a cast that fails although "all the elements are right", comes from the two differing, so when a check misbehaves, ask what the object was **created** as, not what the variable says. ## Runtime cost and caveats - Every generic object carries its type arguments, and `is`/`as` checks against generic types do real work. The cost is small but not zero, so avoid heavy `is List<Foo>` checks in hot loops. - The printed form of a `Type`, such as `T.toString()`, is fine for debugging but should not be used as a stable identifier; web builds can minify names. - The runtime type argument is fixed when the object is created. A `List<dynamic>` whose elements happen to all be strings is still not a `List<String>`, which is the source of a classic JSON-parsing bug. ## Summary - Dart keeps type arguments, so generic `is` checks, `is T`, `as T` and `<T>[]` behave as they read. - `T` is a type, not a class: no `T()`, no `T.staticMember`. - Pass factories or callbacks when generic code must create or parse values.

  • Why does `T.fromJson(json)` not compile inside a generic Page<T>, and what do you do instead?
    A type parameter is a type, not a class reference, and Dart has no static or constructor access through type variables. Pass the behaviour in: a `T Function(Map<String, dynamic>) fromJson` parameter, called for each item. Callers supply `Order.fromJson` as a constructor tear-off.
  • A `List<Object>` variable holds a list created as `<String>['a']`. What does `is List<String>` return?
    `true`. The check looks at the object's runtime type, which was fixed when the list was created as a `List<String>`; the variable's static type does not change it. The reverse also holds: a list created as `List<Object>` holding only strings is not a `List<String>`.

saying these in an interview costs you the question

  • Dart erases type arguments at run time, like Java.
  • Inside a generic class, T() creates a new instance of the type argument.
  • value is T inside generic code always returns true.
  • A List<dynamic> becomes a List<String> once all its elements are strings.
  • T.values or T.fromJson can be called when T is bound to a suitable class.