Lesson 9 · 9 of 20
Generics · Dart
Write generic classes and functions in Dart, with bounded types (extends) and type inference, using an in-memory repository and a groupBy function.

Generics let you write a class or a function once and use it with any type, without losing type checking. You probably use them every day already: List<String> and Map<int, T> are generic classes.
Repository<T extends Entity> is an in-memory repository for any entity. T is a type parameter and extends Entity bounds it: only types that have an id qualify, and thanks to that save can use item.id as the key. A Repository<Customer> stores customers and nothing else; customers.save('Elena') would not compile. findById returns T? because the customer may not exist, and for id 9 it gives null.
groupBy<K, V> is a generic function: it takes items of type V and a function that gets a key of type K from each one. You do not have to write the types when calling it, because Dart infers them from the arguments: with customers and c.city, K is String; with words and s.length, K is int. The ??= operator creates each group's list the first time its key turns up.
Unlike Java, Dart keeps generic types at run time (they are reified): runtimeType prints Repository<Customer>, and a check such as is List<int> genuinely works.
Code · main.dart
abstract class Entity { int get id; }class Customer implements Entity { @override final int id; final String name, city; Customer(this.id, this.name, this.city); @override String toString() => name;}// One generic class, valid for any type that is an Entity.class Repository<T extends Entity> { final Map<int, T> _items = {}; void save(T item) => _items[item.id] = item; T? findById(int id) => _items[id]; List<T> get all => _items.values.toList();}// A generic function: K and V are inferred from the arguments.Map<K, List<V>> groupBy<K, V>(Iterable<V> items, K Function(V) keyOf) { final groups = <K, List<V>>{}; for (final item in items) (groups[keyOf(item)] ??= []).add(item); return groups;}void main() { final customers = Repository<Customer>() ..save(Customer(1, 'Elena', 'Madrid')) ..save(Customer(2, 'Jorge', 'Malaga')) ..save(Customer(3, 'Nuria', 'Madrid')); print('Found: ${customers.findById(2)}, missing: ${customers.findById(9)}'); print(groupBy(customers.all, (c) => c.city)); print(groupBy(['kiwi', 'fig', 'pear', 'plum'], (s) => s.length)); print('Type kept at runtime: ${customers.runtimeType}');}Output · dart run main.dart
Found: Jorge, missing: null
{Madrid: [Elena, Nuria], Malaga: [Jorge]}
{4: [kiwi, pear, plum], 3: [fig]}
Type kept at runtime: Repository<Customer>Key points
<T>is a type parameter, fixed when the class is used.T extends Baserestricts the types and lets you useBase's members.- Functions can be generic too, and Dart infers their types.
- Dart's generics are kept at run time.
Exercise
Create a Product implements Entity class with id, name and price, store three in a Repository<Product> and group them with groupBy by whether they cost more or less than 20 euros.