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 aList<dynamic>and turns off type checking. Write<String>[]instead.