Skip to content
AgileLogics
5 min read

Generics

Prerequisites: Classes and objects

Generics let you write one function or class that works with many types while keeping full type safety. List<String> and Map<String, int> are generic types you've used all along.

A generic function

(T, T)? firstAndLast<T>(List<T> items) {
  if (items.isEmpty) return null;
  return (items.first, items.last);
}

void main() {
  print(firstAndLast([3, 7, 9]));
  print(firstAndLast(['a', 'b']));
  print(firstAndLast(<int>[]));
}
(3, 9)
(a, b)
null

<T> introduces a type parameter — filled in at each call. Unlike using Object or dynamic, the result keeps its real type.

Constraints

T extends SomeType requires the type to be (a subtype of) SomeType:

T largest<T extends Comparable<T>>(List<T> items) {
  var best = items.first;
  for (final item in items) {
    if (item.compareTo(best) > 0) best = item;
  }
  return best;
}

void main() {
  print(largest([4, 9, 2]));
  print(largest(['pear', 'apple', 'zucchini']));
}
9
zucchini

Without the constraint, compareTo wouldn't be available — not every type can be compared.

A generic class

class Stack<T> {
  final _items = <T>[];

  void push(T item) => _items.add(item);
  T? pop() => _items.isEmpty ? null : _items.removeLast();
  T? get top => _items.isEmpty ? null : _items.last;
  bool get isEmpty => _items.isEmpty;
}

void main() {
  final history = Stack<String>();
  history.push('home');
  history.push('settings');
  print(history.pop());
  print(history.top);
}
settings
home

Generics are reified in Dart: the type argument is kept at runtime, so a List<int> really knows it holds ints (list is List<int> works).

Common mistakes

  • Creating collections without a type, like final items = [];, which makes a List<dynamic> and turns off type checking. Write <String>[] instead.