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?
answer
- default bound is Object?
- members of the bound are callable
- extends Object forbids nullable
- violations are compile errors
- instantiate to bound when omitted
basics
~20 sA 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.
solid answer
~40 sWithout a bound, a type parameter's implicit bound is `Object?`, so generic code can only use `Object?` members on a `T`. Declaring `class Page<T extends Identifiable>` does two things: callers may only pass `Identifiable` or its subtypes, checked at compile time, and the body may call `item.id` on any `T`. `T extends Object` is the idiom for **non-nullable** type arguments, so `Page<Order?>` is rejected. When the type argument is omitted, as in a raw type annotation `Page p` or a call `Registry()` with nothing to infer from, Dart **instantiates to bound**: it uses the bound, so `Registry()` becomes `Registry<Identifiable>`, and for an unbounded parameter a raw type becomes `dynamic`, which is why a raw `List` means `List<dynamic>`.
code
dart · 33 linesabstract interface class Identifiable {
String get id;
}
class Order implements Identifiable {
Order(this.id);
@override
final String id;
}
class Page<T extends Identifiable> {
Page(this.items);
final List<T> items;
Page<T> mergeWith(Page<T> next) {
final seen = {for (final item in items) item.id};
return Page([...items, ...next.items.where((i) => seen.add(i.id))]);
}
}
class Registry<T extends Identifiable> {
final Map<String, T> _byId = {};
void add(T item) => _byId[item.id] = item;
}
void main() {
final first = Page([Order('a1'), Order('a2')]);
final merged = first.mergeWith(Page([Order('a2'), Order('a3')]));
print(merged.items.map((o) => o.id).toList()); // [a1, a2, a3]
print(Registry().runtimeType); // Registry<Identifiable>: instantiated to bound
// Page<String>([]); // compile-time error: String is not an Identifiable
}go deeper
Recall that extends in a type parameter limits what can be passed and lets you use the bound's members on T.
Explain the Object? default bound, T extends Object for non-null arguments, and how instantiate to bound fills omitted arguments.
Choose between bounds and passed-in functions for generic helpers, and remove raw types that silently introduce dynamic.
Set API conventions for bounds in shared packages, since tightening a bound later breaks callers while loosening it is compatible.
## What a bound is A **bound** is written with `extends` in a type-parameter declaration: `class Page<T extends Identifiable>`. It means *every type argument for `T` must be `Identifiable` or a subtype of it*. The word `extends` is used even when the bound is an interface you would `implements`; in a bound it only means "is a subtype of". ## The two effects of a bound 1. **It restricts callers.** `Page<Order>` compiles if `Order` implements `Identifiable`; `Page<String>` is a compile-time error. 2. **It empowers the body.** Inside `Page`, a value of type `T` is known to be an `Identifiable`, so `item.id` compiles. Without the bound, `T` is only known to be an `Object?` and you would need a cast. ## The default bound and `T extends Object` | Declaration | Allowed type arguments | Members usable on `T` | |---|---|---| | `class Box<T>` | any type, including `String?` and `Null` | those of `Object?`, after a null check those of `Object` | | `class Box<T extends Object>` | non-nullable types only | those of `Object`, such as `hashCode` and `toString()` | | `class Box<T extends num>` | `num`, `int`, `double` | arithmetic and comparison operators | | `class Box<T extends Identifiable>` | `Identifiable` and its subtypes | `id` and the rest of `Identifiable` | `T extends Object` is the standard way to say "no nullable type arguments". It matters for APIs where `null` has a meaning of its own, for example a cache whose `get` returns `T?` to signal a miss: if `T` itself could be nullable, a stored `null` and a miss would be indistinguishable. ## Instantiate to bound Sometimes a generic type is used **without** type arguments and there is nothing to infer them from: - a raw type annotation such as `Page page = ...`; - a constructor call with no arguments, such as `Registry()`; - a raw type in a type test. Dart then **instantiates to bound**: it fills in each missing argument with the parameter's bound. The dart.dev generics page shows `var foo = Foo();` printing `Instance of 'Foo<SomeBaseClass>'` for `Foo<T extends SomeBaseClass>`. For an **unbounded** parameter the result is `dynamic`, so a raw `List` means `List<dynamic>` and a raw `Map` means `Map<dynamic, dynamic>`. That silently turns off type checking for everything read from it, which is why teams enable the analyzer's strict raw-types mode. ## Using bounds in the paginated scenario Merging two pages must drop items already seen. With `T extends Identifiable`, the merge can key on `id` directly: ```dart Page<T> mergeWith(Page<T> next) { final seen = {for (final item in items) item.id}; return Page([...items, ...next.items.where((i) => seen.add(i.id))]); } ``` Without the bound this would need a cast or a key-extractor function passed in; both are valid designs, and the bound is the tighter one when every model already has an `id`. ## Bound versus passed-in function A bound is not the only way to let generic code use a member of `T`. Compare two ways to deduplicate by id: | Approach | Signature | Trade-off | |---|---|---| | bound | `class Page<T extends Identifiable>` | concise; every model must implement `Identifiable` | | key extractor | `Page<T> mergeWith(Page<T> next, String Function(T) idOf)` | works for any `T`, including types you do not own; one more argument | Choose the bound when the interface is already part of your domain model, and the function when the types come from packages you cannot change or when different callers need different keys. ## Multiple type parameters and bounds referring to others - Each parameter has its own bound: `class Cache<K extends Object, V>`. - A bound can mention other parameters: `class Grouped<K, T extends Keyed<K>>`. - A bound can mention the parameter itself, `T extends Comparable<T>`, which is an **F-bound**; it has its own traps with `int` and `num`. ## Common mistakes - Writing a bound only to call one method, when a function parameter would be more flexible for callers. - Forgetting that the default bound is `Object?`, then being surprised that `Page<Order?>` compiles. - Leaving raw types such as `List` or `Map` in signatures and getting `dynamic` everywhere they are read.
- Why would a cache declare `class Cache<V extends Object>` when its `get` method returns a nullable result?With an unbounded `V`, someone could create a `Cache<String?>` and store `null`; then `get` returning `null` could mean either a miss or a stored `null`. Bounding `V` by `Object` makes every stored value non-null, so `null` from `get` unambiguously means the key is absent.
- What type does a raw `Map` annotation get, and why is that risky?Its parameters are unbounded, so instantiating to bound gives `Map<dynamic, dynamic>`. Everything read from it is `dynamic`, so misspelled members and wrong types are only caught at run time. The analyzer's strict raw-types mode flags these annotations so they get explicit type arguments.
saying these in an interview costs you the question
- An unbounded type parameter only accepts non-nullable types.
- Using extends in a bound requires the argument to subclass it, not implement it.
- A bound is only a hint; violating it fails at run time.
- A raw List annotation means List<Object>.
- Without a bound you can still call any method on T.