In Dart, how would you use an extension type so user ids and order ids, both plain ints, cannot be mixed up, and what does it cost at runtime?
answer
- extension type UserId(int value)
- representation type
- distinct static type
- no implements int: opaque
- erased: zero allocation
basics
~20 sDeclare extension type UserId(int value) {} and extension type OrderId(int value) {}. Each is a distinct static type over int, so passing a UserId where an OrderId or int is expected is a compile error, yet at runtime both are just int: no wrapper object.
solid answer
~40 sAn extension type is a compile-time view of an existing **representation type**. `extension type UserId(int value) {}` declares a new static type whose values are `int`s, with a constructor `UserId(7)` and a getter `value`. Without an `implements` clause it is **opaque**: `int` members are not available on it, it is not assignable to `int`, and `UserId` and `OrderId` are not assignable to each other, so `cancelOrder(UserId(7))` fails to compile. Calls on it resolve statically, like extension methods, and the type is erased during compilation, so a `List<UserId>` is a `List<int>` at runtime with no per-element allocation. That makes it cheaper than a wrapper class. Adding `implements int` would make it transparent and let ids flow into `int` code, which defeats the purpose here.
code
dart · 20 linesextension type UserId(int value) {
String toPath() => '/users/$value';
}
extension type OrderId(int value) {
String toPath() => '/orders/$value';
}
Future<void> cancelOrder(OrderId id) async {
print('DELETE ${id.toPath()}');
}
Future<void> main() async {
final user = UserId(7);
// await cancelOrder(user); // error: UserId can't be assigned to OrderId
// await cancelOrder(7); // error: int can't be assigned to OrderId
// int raw = user; // error: UserId isn't an int statically
await cancelOrder(OrderId(9)); // DELETE /orders/9
print(user.value + 1); // 8: value is typed int
}go deeper
Recall that extension type UserId(int value) creates a separate compile-time type over int, so ids of different kinds cannot be swapped.
Explain the representation type, the implicit constructor and getter, and how implements switches between an opaque and a transparent extension type.
Choose between extension types and wrapper classes by weighing zero allocation against the lack of runtime guarantees, and put validation in constructors.
Decide where a codebase standardises typed ids and units, balancing compile-time safety, JSON boundaries and the cost of a type that erases at runtime.
## The problem: primitive ids are interchangeable A Flutter app's API layer often carries several kinds of numeric ids: user ids, order ids, product ids. As plain `int`s they are interchangeable, so nothing stops this bug: ```dart Future<void> cancelOrder(int orderId) async { /* ... */ } cancelOrder(user.id); // compiles, cancels the wrong thing ``` A classic fix is a **wrapper class** (`class UserId { final int value; ... }`), which is type-safe but allocates an object per id and needs `==` and `hashCode` written. Dart 3.3 added **extension types**, which give the type safety without the allocation. ## Declaring the extension types ```dart extension type UserId(int value) {} extension type OrderId(int value) {} ``` Each declaration: - names a new **static type** (`UserId`), not an alias for `int`; - declares the **representation type** `int` and a **representation variable** `value`, with an implicit getter `int get value` and an implicit constructor `UserId(int value)`; - has an empty body here, so it exposes **no `int` members** at all. In Dart 3.13 the parenthesised clause is a **primary constructor** whose single parameter is always declaring, so the syntax above is unchanged. ## What the compiler now rejects ```dart Future<void> cancelOrder(OrderId id) async { /* ... */ } final user = UserId(7); cancelOrder(user); // error: UserId is not an OrderId cancelOrder(7); // error: int is not an OrderId int raw = user; // error: UserId is not assignable to int user + 1; // error: UserId has no operator + cancelOrder(OrderId(9)); // OK print(user.value); // OK: 7, typed as int ``` Because the extension type does not implement `int`, it is **opaque**: a distinct type with only the members you declare. You can add exactly the ones that make sense, e.g. `bool get isGuest => value == 0;` or `String toPath() => '/users/$value';`. ## `implements`: opaque versus transparent | Declaration | `int` members available? | Assignable to `int`? | Use | |---|---|---|---| | `extension type UserId(int value) {}` | no | no | ids that must not mix | | `extension type UserId(int value) implements int {}` | yes | yes | extend `int` with extras | | `extension type UserId(int value) implements Object {}` | only `Object`'s | to `Object` | non-nullable marker | An extension type may implement only its representation type, a supertype of it, or another extension type on a compatible representation. For ids, stay opaque: `implements int` would let a `UserId` flow into any `int` parameter, including `cancelOrder(int)`. ## What it costs at runtime - **No allocation.** Extension types are erased during compilation; a `UserId` is the `int` itself. The SDK changelog calls them zero-cost wrappers, and dart.dev notes they avoid the expense of wrapping lots of objects. - **No dynamic dispatch.** Member calls resolve at compile time from the static type, like extension methods. - **Collections are free.** `List<UserId>` is exactly `List<int>` at runtime, so decoding a thousand ids allocates no wrappers. ## Construction and validation The representation clause is a constructor, and you can add others or hide it: ```dart extension type const UserId._(int value) { factory UserId(int value) { if (value <= 0) throw ArgumentError.value(value, 'value', 'must be positive'); return UserId._(value); } } ``` Any validation lives in constructors. A cast such as `7 as UserId` does **not** call them, which is the main weakness of the abstraction. ## Limits worth knowing 1. Extension types cannot declare **instance fields** (the representation is the only state) or **abstract members**. 2. They cannot use **`extends`** or **`with`**, and class modifiers do not apply to them. 3. Runtime checks see the representation: `UserId(7) is int` is `true`. ## When to choose it over a wrapper class Choose an extension type for high-volume, performance-sensitive values where compile-time separation is enough — ids, units, interop handles. Choose a wrapper class when you need runtime guarantees, such as `is UserId` distinguishing a user id from an order id, or validation that no cast can bypass.
- In Dart, why not just use `typedef UserId = int;` for ids?A typedef is only another name for the same type: a `UserId` typedef is an `int`, so `cancelOrder(userId)` with an `int` parameter compiles and nothing is separated. An extension type declares a new static type that is not assignable to or from `int` unless it implements it.
- In Dart, when would you add `implements int` to an extension type?When you want to extend `int` rather than hide it: the extension type keeps every `int` member and stays assignable to `int`, and adds its own members on top. That is useful for units or helpers, but wrong for ids that must not mix, because a transparent `UserId` can flow into any `int` parameter.
An extension type is a coloured label stuck on the same box: the warehouse scanner (the compiler) refuses to put a blue-labelled box on the red shelf, but the box inside is unchanged and the label costs nothing to ship.
saying these in an interview costs you the question
- An extension type allocates a small wrapper object for each value.
- typedef UserId = int gives the same type safety as an extension type.
- An extension type without implements still exposes all int members.
- UserId and OrderId over int are interchangeable at compile time.
- Extension types can declare extra instance fields for metadata.