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 like Locker<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:

  1. Why generics are better than dynamic.
  2. How to write generic classes with one or more type parameters.
  3. How to write generic functions, and how Dart infers T.
  4. How to limit T with extends.
  5. 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. dynamic turns 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'.
dynamicGenerics <T>
Works with many typesyesyes
Dart checks types before runningnoyes
Editor knows the methodsnoyes
Bugs foundlate, while runningearly, 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 a String, so toUpperCase() is allowed.
  • marks.open() gives an int, so + 8 is allowed.
  • Naming: T for Type, E for Element, K and V for 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:

TypeHolds
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 is List<int>, so T is int.
  • zip(['Maths'], [92]): K is String, V is int.
  • 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 a T.
  • 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 > b and sum + v work, because T is surely a number.
  • Stats<String>(['a']) is an error before running: String is not a num.

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 fits X and its children.

Simple lesson: generic helpers like Result<T> and Repository<T> are written once and reused everywhere.

TL;DR

  1. Generics = a type parameter <T> filled in when the code is used.
  2. Better than dynamic: Dart checks the type before running.
  3. Generic class: class Box<T>. Several: class Pair<A, B>.
  4. Generic function: T lastOf<T>(List<T> xs). Dart usually infers T.
  5. Bound: <T extends num> lets you use num members on T.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Using dynamic for reuseIt is the same as genericsdynamic turns off type checksBugs appear only while running.
T lastOf(List<T> xs) without <T> after the nameT is knownDeclare it: lastOf<T>(...)A compile error: T is not a type.
a > b on an unbounded TEvery T can compareAdd a bound: <T extends num>A compile error.
Box<String> given an intThe box takes anythingEach box takes only its TA compile error.
One-letter names for everythingAny letter is fineT, E, K, V by custom, longer names when clearerConfusing 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. dynamic gives only reuse.
  • Write generic classes as class Box<T>, and generic functions as T 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 on T.

Next: Pattern Matching. You will bundle values with records, and take data apart in one step.