In Dart, how do you declare a generic class and a generic method, for example a Page<T> wrapper for paginated API responses?
answer
- angle brackets after the name
- T in fields, parameters, return types
- method-level type parameters
- inferred at the call site
- typedef aliases shorten long types
basics
~20 sPut 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 sA 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 linesclass 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
Recall the syntax for a generic class and a generic method, and that callers can pass type arguments or let Dart infer them.
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.
Design generic envelopes such as Page<T> so that mapping, merging and parsing stay typed without casts, and review code for dynamic leaking in.
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.