Skip to main content

Menu

System onlineAI Tutorials / 2026

AI Tutorials
Knowledge search

Recent · Popular · Tutorials

  • dart3 tutorials
  • java1 tutorial
  • Javascript3 tutorials
  • Node.js Introduction2 tutorials
  • dartdart · General · In depth
  • JavascriptJavascript · Advanced · In depth
  • JavascriptJavascript · Advanced · Mid
  • JavascriptJavascript · Advanced · Short

Find a tutorial

Tell the assistant what you want to learn. It suggests published chapters.

For example: “I want to learn Rust ownership from the start.” The conversation is kept only in this tab.

0 / 2,000 characters

Enter to send, Shift+Enter for a new line. Do not include personal information.

Chapter 14

Generics: Writing Type-Safe Reusable Code

You have already used generics without noticing. When you wrote `List<String>` or `Map<String, int>`, the angle brackets were generics at work. This chapter explains what those brackets really mean, why they exist, and how to write your own generic functions and classes so you can reuse the same cod
Level: GeneralLength: In depth4 min read

Read at another depth

Generics: Writing Type-Safe Reusable Code

You have already used generics without noticing. When you wrote List<String> or Map<String, int>, the angle brackets were generics at work. This chapter explains what those brackets really mean, why they exist, and how to write your own generic functions and classes so you can reuse the same code with many types — safely.

The Problem Generics Solve

Imagine you want a small container that holds one item. Without generics, you have two bad options.

Option one: make a container that holds Object, the type that every Dart value has.

Code example in dart
class Box {
  Object? _item;
  void put(Object item) => _item = item;
  Object? get item => _item;
}

void main() {
  final box = Box();
  box.put('hello');
  final item = box.item;
  // The compiler only knows this is Object?, not String.
  // We must "cast" it, which trusts us at run time:
  print((item as String).toUpperCase());
}

The cast as String is a promise to the compiler: "trust me, it really is a String." If that promise is wrong, the program crashes when it runs. The compiler cannot help you.

Option two: write one container per type — StringBox, IntBox, DateBox — and copy-paste the same logic into each. Now your code grows, and a bug fix must be repeated everywhere.

Generics give a third option: write the container once, with a placeholder for the type, and fill in the placeholder each time you use it.

Code example in dart
class Box<T> {
  T? _item;
  void put(T item) => _item = item;
  T? get item => _item;
}

void main() {
  final stringBox = Box<String>();
  stringBox.put('hello');
  print(stringBox.item?.toUpperCase()); // no cast needed

  final intBox = Box<int>();
  intBox.put(42);
  print(intBox.item);
}

T is a type variable — a name that stands for "some type, chosen later." Box<String> is a parameterized type: the Box recipe with T filled in as String.

What "Type-Safe" Buys You

Type-safe means the compiler checks that the types line up before the program runs. Compare the two boxes:

Code example in dart
final safeBox = Box<String>();
safeBox.put(42); // compile-time error: caught before running

final riskyBox = OldBox();
riskyBox.put(42);
riskyBox.item as String; // run-time crash: caught only when it hurts

Errors that surface at compile time cost seconds to fix. Errors that surface at run time may reach real users. Generics move whole categories of mistakes from the second group to the first.

Generic Functions

Functions can have type variables too. Here is a firstOrNull helper that works on any list:

Code example in dart
T? firstOrNull<T>(List<T> items) {
  if (items.isEmpty) return null;
  return items[0];
}

void main() {
  print(firstOrNull<String>(['a', 'b'])); // a
  print(firstOrNull<int>([]));             // null
  print(firstOrNull(['x', 'y']));          // Dart infers T as String
}

The return type T? says: "I return either a T or nothing." In the last call we never wrote <String>; Dart inferred it from the argument's type, just as it infers variable types. Most of the time you can leave the angle brackets off a function call.

A more interesting example — swapping the first two elements of a list, for any element type:

Code example in dart
void swapFirstTwo<T>(List<T> items) {
  if (items.length < 2) return;
  final first = items[0];
  items[0] = items[1];
  items[1] = first;
}

void main() {
  final words = ['a', 'b', 'c'];
  swapFirstTwo(words);
  print(words); // [b, a, c]
}

Try writing this with Object instead of T: it would accept the list but then refuse to put an Object back into a List<String>. The type variable is what makes the function honest about what it takes and returns.

Generic Classes: A Real Worked Example

Let us build a small Cache<K, V> — a place to store values under keys. It uses two type variables at once, exactly like Dart's built-in Map<K, V>:

Code example in dart
class Cache<K, V> {
  final Map<K, V> _store = {};

  void put(K key, V value) => _store[key] = value;

  V? get(K key) => _store[key];

  V getOrPut(K key, V Function() makeValue) {
    final existing = _store[key];
    if (existing != null) return existing;
    final created = makeValue();
    _store[key] = created;
    return created;
  }
}

void main() {
  final userNames = Cache<int, String>();
  userNames.put(1, 'Ada');
  print(userNames.get(1));              // Ada
  print(userNames.get(2));              // null
  print(userNames.getOrPut(2, () => 'Grace')); // Grace, then cached
}

Notice getOrPut: it takes a function that makes a value, so an expensive value is computed only when it is actually missing. Also notice V? as the return of get — a generic class can still combine with null safety. A key that is absent returns "no value," and the compiler forces callers to handle that.

Constraints: Limiting What T Can Be

Sometimes you need to assume something about T. Suppose you want a function that returns the larger of two values. Comparison requires a method, so we constrain T with the extends keyword:

Code example in dart
class Named {
  final String name;
  Named(this.name);
  @override
  String toString() => name;
}

T pickLarger<T extends Comparable>(T a, T b) {
  return a.compareTo(b) >= 0 ? a : b;
}

void main() {
  print(pickLarger(3, 7));            // 7
  print(pickLarger('apple', 'pear')); // pear
}

T extends Comparable means: "T can be any type, as long as it is a Comparable" — a type that promises a compareTo method. Inside the function, the compiler now lets you call compareTo, because every legal T has one. Without the constraint, that call would be a compile error.

This is the balance of generics: unconstrained code works with everything but can assume nothing; constrained code gives up some universality to gain some capability.

Where You Will Meet Generics

You will use generics constantly in Dart, mostly on the receiving end:

ConstructMeaning
`List<int>`A list whose elements are all `int`
`Set<String>`A set of unique strings
`Map<String, int>`Keys are strings, values are integers
`Future<String>`An asynchronous operation that produces a string
`Stream<int>`A sequence of asynchronous integer events
`Iterable<T>`A sequence of `T` you can step through

That last two rows matter for later chapters: Future<T> and Stream<T> are generic classes, so everything you learned here applies directly to asynchronous code. A Future<String> is not "a future of anything" — it is specifically a promise of a string, and the compiler will hold you to that.

Exercises

Exercise

Write a generic function lastOf<T> that takes a List<T> and returns T? — the last element, or null if the list is empty. Then call it once with a List<String> and once with a List<double>, letting Dart infer the type.

Show answer

T? lastOf<T>(List<T> items) => items.isEmpty ? null : items[items.length - 1]; void main() { print(lastOf(['a', 'b'])); // b print(lastOf([1.5, 2.5])); // 2.5 print(lastOf<String>([])); // null }

Exercise

What is the difference between class Box { Object? _item; } and class Box<T> { T? _item; } from the caller's point of view?

Show answer

With Object?, every box accepts anything, and callers must cast values back to the type they expect — risking a run-time crash if the cast is wrong. With Box<T>, each caller picks one type (e.g. Box<String>), the compiler rejects wrong-type values at compile time, and no casts are needed because values come back already typed as T.

Exercise

Write a generic class Pair<A, B> holding two values of possibly different types, with a swap() method that returns a new Pair<B, A>. Use it to pair a name with an age and then swap it.

Show answer

class Pair<A, B> { final A first; final B second; Pair(this.first, this.second); Pair<B, A> swap() => Pair(second, first); } void main() { final p = Pair<String, int>('Ada', 36); final swapped = p.swap(); // Pair<int, String> print('${swapped.first} ${swapped.second}'); // 36 Ada }

Spotted a mistake, or code that does not work?

Report an issue

Chapter: Generics: Writing Type-Safe Reusable Code

0 / 2,000 characters

Plain text, for example: the second code sample does not compile. Do not include personal information.

You can't do that