Generics

Dart · Practical · 10 min

Typed collections, Futures and Flutter widgets like ValueListenableBuilder<T> are generic, and you'll write your own.

Generics let one class or function work with many types while keeping type safety: List<String>, Map<String, int>, Future<User>.

Declare type parameters in angle brackets:

class Box<T> {
  final T value;
  Box(this.value);
}
T firstOr<T>(List<T> items, T fallback) => items.isEmpty ? fallback : items.first;

Type arguments are usually inferred: Box(3) is a Box<int>. Write them explicitly when inference has nothing to go on, as in <String>[] or Future<void>.delayed(...).

Bounds restrict type parameters: class SortedList<T extends Comparable<T>> can call compareTo on elements. Without a bound, T can only use Object? members.

Dart generics are reified: type arguments exist at run time. list is List<int> is a real check, and a List<int> won't accept a String even through a cast. (Contrast with Java's erasure.)

Generic classes are covariant: a List<int> is a List<num>. This is convenient but not fully static — adding a double to a List<num> that is really a List<int> throws at run time. The analyzer can warn about such cases with strict settings.

Use generics to remove duplication, but don't make APIs generic when one concrete type would do.

Example

class Cache<K, V> {
  final _store = <K, V>{};
  final V Function(K) _load;
  int misses = 0;
  Cache(this._load);
  V get(K key) => _store.putIfAbsent(key, () {
        misses++;
        return _load(key);
      });
}
T maxOf<T extends Comparable<T>>(List<T> items) =>
    items.reduce((a, b) => a.compareTo(b) >= 0 ? a : b);

void main() {
  final lengths = Cache<String, int>((s) => s.length);
  print(lengths.get('dart') + lengths.get('dart'));  // 8
  print(lengths.misses);                             // 1

  print(maxOf(['pear', 'apple', 'zebra']));          // zebra
  print(maxOf<num>([3, 9, 4]));  // 9 (int is Comparable<num>, not <int>)

  Object things = <int>[1, 2];
  print(things is List<int>);                        // true (reified)
  print(things is List<String>);                     // false
}

Remember

  • Type parameters keep code reusable and type-safe
  • T extends Bound lets you call the bound's members
  • Generics are reified: is List<int> is checked at run time

Watch out for

  • Relying on covariance and adding a wider type to a narrower list, which throws at run time.

Check yourself

  1. What is printed by `Object x = <int>[]; print(x is List<String>);`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: dart.dev/language/generics