Generics in Dart
By Syncfusion®, Inc. · Last updated
How to write classes and functions with type parameters like T, so one piece of code works safely with many types.
What Are Generics?
Generics = writing a class or function with a type parameter, like T, that is filled in later.
- Type parameter = a placeholder for a type, written in angle brackets:
<T>. - Type argument = the real type you give when you use it:
Box<String>. - You already use generics:
List<int>,Map<String, int>. Now you will write your own.
Simple lesson: generics let you write code once and use it safely with any type.
A Real-World Example
The school has lockers. Every locker works the same way: open, put in, take out.
- Locker 1 holds books. Locker 2 holds sports kit. Locker 3 holds lunch boxes.
- You do not build a new kind of locker for each thing.
- But a locker labelled "books" should refuse a cricket bat.
- A generic class is that locker:
Locker<Book>works likeLocker<Bat>, and each accepts only its own type.
Choose T, then try to put things in the box:
Every Run box on this page runs on the course's Dart runner in your browser.
The runner does not check type arguments. Wrong types are caught by real Dart before running, so those examples are shown in plain boxes.
What You Will Learn
In this lesson, you will learn:
- Why generics are better than
dynamic. - How to write generic classes with one or more type parameters.
- How to write generic functions, and how Dart infers
T. - How to limit
Twithextends. - Practical generics:
Box<T>,Result<T>and a small repository.
Why Use Generics?
The Problem with dynamic
Without generics, you might write a box that holds anything:
A dynamic box lets a bug through
class AnyBox {
dynamic item;
AnyBox(this.item);
}
void main() {
var box = AnyBox('pencil');
print(box.item.toUpperCase());
box = AnyBox(42);
print(box.item.toUpperCase());
}
- Dart could not warn you.
dynamicturns off type checks. - The bug appears only when the program runs, maybe in front of a user.
Type Safety and Reuse
With a type parameter, Dart knows what is inside:
class Box<T> {
T item;
Box(this.item);
}
var box = Box<String>('pencil');
box.item = 42; // Error before running: A value of type 'int' can't be assigned to a variable of type 'String'.
dynamic | Generics <T> | |
|---|---|---|
| Works with many types | yes | yes |
| Dart checks types before running | no | yes |
| Editor knows the methods | no | yes |
| Bugs found | late, while running | early, while typing |
Simple lesson: generics give you reuse and safety, while dynamic gives reuse only.
Generic Classes
One Type Parameter
class Name<T> { ... use T as a type ... }
A box for anything, checked by type
class Box<T> {
T item;
Box(this.item);
T open() => item;
@override
String toString() => 'Box($item)';
}
void main() {
var pencil = Box<String>('pencil');
var marks = Box<int>(92);
print(pencil.open().toUpperCase());
print(marks.open() + 8);
print([pencil, marks]);
}
pencil.open()gives aString, sotoUpperCase()is allowed.marks.open()gives anint, so+ 8is allowed.- Naming:
Tfor Type,Efor Element,KandVfor Key and Value.
Multiple Type Parameters
A pair of two different types
class Pair<A, B> {
final A first;
final B second;
Pair(this.first, this.second);
@override
String toString() => '($first, $second)';
}
void main() {
var student = Pair<String, int>('Asha', 92);
var seat = Pair<int, String>(14, 'Row C');
print(student);
print(seat);
print(student.first.length);
}
Generic Collections
The collections you know are generic classes:
| Type | Holds |
|---|---|
List<T> | items of type T, in order |
Set<T> | unique items of type T |
Map<K, V> | keys of type K, each with a value of type V |
Simple lesson: a generic class has a type hole, <T>, that each user fills in.
Generic Functions
Writing One
Put <T> after the function name:
One function, many types
T lastOf<T>(List<T> items) => items[items.length - 1];
Map<K, V> zip<K, V>(List<K> keys, List<V> values) {
var result = <K, V>{};
for (var i = 0; i < keys.length; i++) {
result[keys[i]] = values[i];
}
return result;
}
void main() {
print(lastOf<String>(['Asha', 'Ravi', 'Priya']));
print(lastOf([10, 20, 30]));
print(zip(['Maths', 'Science'], [92, 85]));
}
Type Inference
Type inference = Dart works out T from the values you pass, so you can leave out <...>.
lastOf([10, 20, 30]): the list isList<int>, soTisint.zip(['Maths'], [92]):KisString,Visint.- Write
<T>yourself only when Dart cannot guess, or to make the code clearer.
How Dart fills in T
Simple lesson: a generic function adapts to the types you pass, and Dart usually guesses T for you.
Bounded Type Parameters
Bound = <T extends SomeType> means T must be SomeType or one of its children.
- Inside the class, you can then use
SomeType's members on aT. - Without a bound, Dart only lets you use what every object has.
Only numbers can be averaged
class Stats<T extends num> {
final List<T> values;
Stats(this.values);
T get biggest => values.reduce((a, b) => a > b ? a : b);
String get average {
var total = values.fold<num>(0, (sum, v) => sum + v);
return (total / values.length).toStringAsFixed(1);
}
}
void main() {
var marks = Stats<int>([72, 95, 88]);
print(marks.biggest);
print(marks.average);
var heights = Stats<double>([1.42, 1.55, 1.38]);
print(heights.biggest);
}
a > bandsum + vwork, becauseTis surely a number.Stats<String>(['a'])is an error before running:Stringis not anum.
A bound can also be your own class: class Team<T extends Student>.
Then every member of the team is sure to have name, rollNo and the other Student members.
Simple lesson: extends on a type parameter says "any type, as long as it is this kind".
Practical Generics
A Result Type
Result<T> holds either a value of type T or an error message:
Result<int> for safe parsing
sealed class Result<T> {}
class Ok<T> extends Result<T> {
final T value;
Ok(this.value);
}
class Err<T> extends Result<T> {
final String message;
Err(this.message);
}
Result<int> readMarks(String text) {
var n = int.tryParse(text);
if (n == null) return Err('Not a number: $text');
if (n < 0 || n > 100) return Err('Out of range: $n');
return Ok(n);
}
void main() {
for (var text in ['88', 'abc', '140']) {
var r = readMarks(text);
switch (r) {
case Ok(value: var v):
print('Saved $v');
case Err(message: var m):
print('Problem: $m');
}
}
}
A Simple Repository
Repository = a class that stores and finds items. A generic one works for students, books or anything else:
One repository class, two uses
class Repository<T> {
final Map<int, T> _items = {};
void add(int id, T item) => _items[id] = item;
T? find(int id) => _items[id];
int get count => _items.length;
}
void main() {
var students = Repository<String>();
students.add(1, 'Asha');
students.add(2, 'Ravi');
var fees = Repository<int>();
fees.add(1, 1200);
print(students.find(2));
print(students.find(9));
print(fees.find(1));
print('${students.count} students, ${fees.count} fee record');
}
find returns T?, because the id may not exist. (See Null Safety.)
Do not reach for dynamic or Object when you mean "any one type, chosen by the user". That is exactly what <T> is for.
Easy way to remember
<T>after a class name: a type hole for the whole class.<T>after a function name: a type hole for one call.<T extends X>: the hole only fitsXand its children.
Simple lesson: generic helpers like Result<T> and Repository<T> are written once and reused everywhere.
TL;DR
- Generics = a type parameter
<T>filled in when the code is used. - Better than
dynamic: Dart checks the type before running. - Generic class:
class Box<T>. Several:class Pair<A, B>. - Generic function:
T lastOf<T>(List<T> xs). Dart usually infersT. - Bound:
<T extends num>lets you usenummembers onT.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
Using dynamic for reuse | It is the same as generics | dynamic turns off type checks | Bugs appear only while running. |
T lastOf(List<T> xs) without <T> after the name | T is known | Declare it: lastOf<T>(...) | A compile error: T is not a type. |
a > b on an unbounded T | Every T can compare | Add a bound: <T extends num> | A compile error. |
Box<String> given an int | The box takes anything | Each box takes only its T | A compile error. |
| One-letter names for everything | Any letter is fine | T, E, K, V by custom, longer names when clearer | Confusing code. |
Check Yourself
Exercises
What to Remember
- Generics add a type parameter like
<T>that is filled in when the code is used. - They give reuse and type safety.
dynamicgives only reuse. - Write generic classes as
class Box<T>, and generic functions asT f<T>(...). - Dart usually infers the type argument from the values you pass.
- A bound like
<T extends num>lets you use that type's members onT.
Next: Pattern Matching. You will bundle values with records, and take data apart in one step.