Generics: Writing Type-Safe Reusable Code
A generic is a way to write one piece of code that works with many types, while still keeping Dart's type checks. Instead of writing a separate "box for strings" and "box for numbers," you write one "box of T" — where T is a type variable, a placeholder filled in later.
Why Generics Matter
Imagine storing items in a plain list without type information. You could put anything in it — and then you'd have to remember what you put there. If you forget, the program can fail when you pull items back out.
Generics solve this: Dart knows what type each collection holds, and it checks that you use it correctly. You have already used generics! Dart lists are actually List<String>, List<int>, and so on.
void main() {
// A list that can only hold strings.
final names = <String>[];
names.add('Ada');
// names.add(42); // Error! 42 is not a String.
final numbers = <int>[1, 2, 3];
print(numbers.length);
}Writing Your Own Generic Function
Here is a function that returns the first element of any list:
T firstOf<T>(List<T> items) {
if (items.isEmpty) {
throw StateError('List is empty');
}
return items.first;
}
void main() {
print(firstOf<String>(['a', 'b'])); // a
print(firstOf<int>([10, 20])); // 10
}The <T> after the function name says "this function has a type variable called T." You pass the type in the same way you pass values, and Dart usually infers it for you.
A Generic Class
Classes can have type variables too. Here is a simple box that holds one value of any type:
class Box<T> {
final T value;
Box(this.value);
}
void main() {
final name = Box<String>('Ada');
final year = Box<int>(1815);
print(name.value);
print(year.value);
}Show diagram source
graph LR
A["Box<T>"] --> B["Box<String>"]
A --> C["Box<int>"]
A --> D["Box<bool>"]Constraining the Type
Sometimes you want to limit T to certain types. The keyword extends sets a boundary:
class Shape {
double area() => 0;
}
class Circle extends Shape {
final double radius;
Circle(this.radius);
@override
double area() => 3.14159 * radius * radius;
}
// T must be a Shape (or a subclass of Shape).
T biggest<T extends Shape>(List<T> shapes) {
shapes.sort((a, b) => b.area().compareTo(a.area()));
return shapes.first;
}
void main() {
final circle = biggest([Circle(1), Circle(3)]);
print(circle.area()); // about 28.27
}Because of the constraint, Dart knows every T has an area() method.
Generics let you write code once, use it everywhere, and let the compiler catch mistakes before your program ever runs.
Exercise
What does Box<int> mean, and what happens if you try Box<int>('hi')?
Show answer
Box<int> is the class Box filled in with the type int, so it can only hold integer values. Passing the string 'hi' is a type error and the program will not compile.
Spotted a mistake, or code that does not work?