skip to content

Generic Types

Dart generics are reified, so List<int> survives to run time, and covariant by default, so some unsound writes fail only at run time. Interviewers probe bounds, variance and inference.

part ofDartoverview, primer and where to startread it →
on this pageshow

explore

questions

9

In Dart, how do you declare a generic class and a generic method, for example a Page<T> wrapper for paginated API responses?

level: juniorimportance: must knowfreq 60%

answer

  1. angle brackets after the name
  2. T in fields, parameters, return types
  3. method-level type parameters
  4. inferred at the call site
  5. typedef aliases shorten long types

basics

~20 s

Put type parameters in angle brackets after the name: class Page<T> { final List<T> items; } or R transform<R>(...). Callers pass Page<Order> or let inference pick the argument, and every use of T is then checked statically.

solid answer

~40 s

A generic class declares type parameters after its name, `class Page<T>`, and uses `T` wherever a concrete type would go: `final List<T> items;`, `T get first`, constructor parameters. A generic method or function declares its own parameters, `Page<R> map<R>(R Function(T item) convert)`, independent of the class's `T`. Callers either pass type arguments explicitly, `Page<Order>(...)`, or let Dart infer them from arguments and context. One declaration then serves orders, products and messages with full static checking, instead of a `Page` per model or a `Page` of `Object?` that needs casts. For long generic types, a non-function **type alias** such as `typedef JsonMap = Map<String, dynamic>;` or `typedef PageFuture<T> = Future<Page<T>>;` gives a shorter name without creating a new type.

code

dart · 26 lines
dart
class Page<T> {
  const Page({required this.items, this.nextCursor});

  final List<T> items;
  final String? nextCursor;

  bool get hasMore => nextCursor != null;

  Page<R> map<R>(R Function(T item) convert) =>
      Page(items: items.map(convert).toList(), nextCursor: nextCursor);
}

typedef JsonMap = Map<String, dynamic>;
typedef PageFuture<T> = Future<Page<T>>;

T firstOr<T>(List<T> items, T fallback) => items.isEmpty ? fallback : items.first;

void main() {
  final names = Page<String>(items: ['ada', 'grace'], nextCursor: 'c2');
  final lengths = names.map((s) => s.length); // Page<int>
  print(lengths.items); // [3, 5]
  print(firstOr(<int>[], 0)); // 0

  JsonMap json = {'id': 'a1'};
  print(json is Map<String, dynamic>); // true: an alias is not a new type
}

go deeper

for a junior

Recall the syntax for a generic class and a generic method, and that callers can pass type arguments or let Dart infer them.

for a middle

Explain the difference between a class's type parameter and a method's own, and what a non-function typedef alias does and does not do.

for a senior

Design generic envelopes such as Page<T> so that mapping, merging and parsing stay typed without casts, and review code for dynamic leaking in.

for a principal

Decide where shared generic abstractions belong in a codebase so they remove duplication without becoming hard-to-read type puzzles.

## Why a paginated API wants generics Most list endpoints return the same envelope: a list of items plus a cursor or page number for the next request. Without generics you either write `OrderPage`, `ProductPage` and `MessagePage` with identical logic, or one `Page` holding `List<Object?>` that forces a cast at every use. A **generic class** writes the envelope once and lets each use say what it holds. ## Declaring a generic class ```dart class Page<T> { const Page({required this.items, this.nextCursor}); final List<T> items; final String? nextCursor; bool get hasMore => nextCursor != null; } ``` - `T` is a **type parameter**: a placeholder that each use of `Page` fills in. - Inside the class, `T` can appear in field types, parameter types, return types, local variables and nested generic types such as `List<T>`. - By convention type parameters are single capital letters: `E` for elements, `K` and `V` for keys and values, `T`, `S`, `R` for other types. - Without an `extends` bound, `T` may be any type, including nullable ones; its implicit bound is `Object?`. ## Declaring a generic method or function Methods and top-level functions can have their own type parameters, declared after the name: ```dart Page<R> map<R>(R Function(T item) convert) => Page(items: items.map(convert).toList(), nextCursor: nextCursor); ``` Here `R` belongs to `map` and is chosen per call, while `T` belongs to the `Page` instance. A generic top-level function looks the same: `T firstOr<T>(List<T> items, T fallback)`. ## Supplying type arguments | Form | Example | Result | |---|---|---| | explicit | `Page<Order>(items: orders)` | `Page<Order>` | | inferred from arguments | `Page(items: <Order>[])` | `Page<Order>` | | inferred from context | `Page<Order> p = Page(items: [])` | `Page<Order>` | | generic method, inferred | `page.map((o) => o.total)` | `Page<double>` when `total` is a `double` | Inference is usually right, but explicit arguments document intent and avoid surprises, such as an empty `[]` literal inferring `List<dynamic>` where no context is available. ## Type aliases for long generic types Since Dart 2.13, `typedef` can name **any type**, not only function types: 1. `typedef JsonMap = Map<String, dynamic>;` gives the decoded-JSON map type a short name. 2. `typedef PageFuture<T> = Future<Page<T>>;` is a **generic alias**: `PageFuture<Order>` means exactly `Future<Page<Order>>`. 3. An alias is **not a new type**. A `Map<String, dynamic>` is a `JsonMap` and vice versa, `is` checks treat them as the same type, and the alias adds no runtime cost. If you need a distinct type, that is a class or an extension type, not a typedef. ## Generic classes in everyday Flutter code You use generic classes long before you write one: - `List<Order>`, `Map<String, Order>` and `Set<String>` from `dart:core`; - `Future<Page<Order>>` returned by a repository method, and `Stream<List<Message>>` from a live query; - `ValueNotifier<int>` and `FutureBuilder<Page<Order>>` in widgets, where the type argument flows into the builder's `snapshot.data`. Writing your own is the same syntax pointed the other way: you declare the placeholder that those classes let callers fill. A good sign that a class should be generic is that its body never looks inside the items it holds, only stores, counts, orders or passes them on. ## What generics buy you - **Static safety**: adding a `Product` to a `Page<Order>`'s item list is a compile-time error. - **No casts** at use sites: `page.items.first.total` is typed. - **One implementation** of merging pages, mapping items or checking `hasMore`. - **Reified type arguments**: a `Page<Order>` remembers `Order` at run time, so `is Page<Order>` checks work. ## Common mistakes - Declaring `class Page<T>` but storing `List<dynamic> items`, which silently opts back out of checking. - Shadowing: declaring `R map<T>(...)` inside `Page<T>` makes the method's `T` hide the class's `T`, a confusing source of type errors. - Expecting a `typedef` to create a distinct type that the compiler keeps apart from the original.

  • Is `typedef OrderId = String;` a way to stop callers passing any String where an order ID is expected?
    No. A type alias is only another name for the same type: every `String` is an `OrderId` and the compiler will not tell them apart. It improves readability but adds no safety. For a distinct type, wrap the value in a class, or use an extension type, which gives a separate static type without a wrapper object.
  • Why give map its own type parameter R instead of reusing the class's T?
    Mapping changes the item type: a `Page<Order>` mapped to totals becomes a `Page<double>`. The result type must be free to differ from `T`, so `map` declares `R`, which the caller's function fixes at each call. Reusing `T` would force the result to be the same type as the input.

saying these in an interview costs you the question

  • A generic class needs a separate subclass for each type it holds.
  • typedef JsonMap = Map<String, dynamic> creates a new, distinct type.
  • A generic method can only use the type parameters of its class.
  • Unbounded type parameters cannot be nullable types.
  • Type arguments must always be written explicitly at the call site.
open as a page

In Dart, what does it mean that generics are reified, and what can code do with a type parameter like T at run time?

level: middleimportance: must knowfreq 50%

basics

~20 s

Reified means every generic object keeps its type arguments at run time, so is List<String> checks, x is T tests and <T>[] literals work. T still cannot be constructed or used to call static members.

open as a page

In Dart, what element types does inference give `var a = [];`, `var b = [1, 2.5];` and `List<int> c = [];`, and why?

level: juniorimportance: should knowfreq 48%

basics

~10 s

a is List<dynamic> because nothing constrains it, b is List<num> because upward inference takes the upper bound of int and double, and c is List<int> because the declared type flows down into the literal.

open as a page

In Dart, what does a type-parameter bound such as T extends Object or T extends Identifiable allow, and what applies when no argument is given?

level: middleimportance: should knowfreq 35%

basics

~20 s

A bound restricts type arguments to subtypes of the bound and lets generic code call the bound's members on T. T extends Object excludes nullable types. When an argument is omitted and nothing infers it, Dart instantiates to the bound.

open as a page

In a Dart Page<T>.fromJson, why does casting json['items'] to List<Map<String, dynamic>> throw even though every element is a map, and how do you fix it?

level: middleimportance: should knowfreq 45%

basics

~10 s

jsonDecode creates a List<dynamic>, and reified generics mean it stays List<dynamic> whatever it contains, so as List<Map<String, dynamic>> fails. Treat it as List<dynamic> and convert each element, or use cast, List.from or whereType.

open as a page

In Dart, why does passing a List<Cat> where a List<Animal> is expected compile, and what happens when the callee then adds a Dog?

level: middleimportance: should knowfreq 42%

basics

~10 s

Dart generic classes are covariant, so List<Cat> is a subtype of List<Animal> and the call compiles. The list keeps its reified List<Cat> type, so add(Dog()) fails a runtime parameter check and throws a TypeError.

open as a page

A Dart model assigns a decoded JSON array to a List<String> field; it compiles but throws at run time. Why, and how do you stop dynamic type arguments leaking?

level: seniorimportance: should knowfreq 36%

basics

~20 s

jsonDecode returns dynamic, so the assignment is an unchecked-at-compile-time downcast, and the decoded array is a List<dynamic> at run time. A List<dynamic> is not a List<String>, so it throws a TypeError even when every element is a String.

open as a page

In Dart, what does the covariant keyword on a parameter do, and why do Flutter overrides like CustomPainter.shouldRepaint rely on it?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

covariant lets an override narrow a parameter to a subtype, which is otherwise an invalid override. The analyzer accepts it and Dart checks the argument at run time instead, so Flutter subclasses can write shouldRepaint(MyPainter oldDelegate).

open as a page

In Dart, what does the F-bound T extends Comparable<T> require, and why does maxOf(3, 7) with that bound infer num rather than int?

level: seniorimportance: nice to knowfreq 18%

basics

~10 s

T extends Comparable<T> requires T to be comparable with itself. int only implements Comparable<num>, so int fails the bound and Dart 3.7+ infers T as num; maxOf<int>(3, 7) is a compile-time error.

open as a page