Generics

21.1 Generic Classes and Methods

class Box<T> {
  T value;
  Box(this.value);
  R map<R>(R Function(T) transform) => transform(value);
}

class Stack<E> {
  final _items = <E>[];
  void push(E item) => _items.add(item);
  E pop() {
    if (_items.isEmpty) throw StateError('Stack is empty');
    return _items.removeLast();
  }
  E? get peek => _items.isEmpty ? null : _items.last;
  bool get isEmpty => _items.isEmpty;
}

T firstOrDefault<T>(List<T> items, T fallback) => items.isEmpty ? fallback : items.first;

void main() {
  final s = Stack<int>()..push(1)..push(2);
  print(s.pop());
  final box = Box('hello');               // Box<String> inferred
  print(box.map((v) => v.length));
  print(firstOrDefault<String>([], 'none'));
}

21.2 Bounded Type Parameters

T maxOf<T extends Comparable<T>>(List<T> items) =>
    items.reduce((a, b) => a.compareTo(b) >= 0 ? a : b);

class Repository<T extends Entity> {
  final Map<String, T> _store = {};
  void save(T entity) => _store[entity.id] = entity;
  T? find(String id) => _store[id];
}

abstract class Entity { String get id; }

num sumAll<T extends num>(Iterable<T> values) => values.fold<num>(0, (a, b) => a + b);

21.3 Reified Generics and Variance

Dart generics are reified: type arguments are available at runtime (list is List<int> works), unlike Java's erasure.

Generic classes are covariant by default (List<int> is a List<num>). That's sound thanks to runtime checks: adding a double to a List<int> seen as List<num> throws. The covariant keyword can relax parameter types in overrides.

List<num> numbers = <int>[1, 2, 3];
// numbers.add(1.5);      // runtime TypeError (covariance check)
print(<int>[] is List<num>);   // true