Extension Types
Prerequisites: Extension methods, Class modifiers
An extension type (Dart 3.3+) wraps an existing type to give it a new compile-time identity and its own API — with no runtime cost. At runtime, the wrapper disappears and only the underlying value remains.
Preventing mix-ups
extension type UserId(int value) {}
extension type OrderId(int value) {}
String findOrder(OrderId id) => 'Order #${id.value}';
void main() {
final order = OrderId(42);
print(findOrder(order));
print(order);
}
Order #42
42
Passing a UserId to findOrder is a compile error, even though
both are "just an int" underneath:
extension type UserId(int value) {}
extension type OrderId(int value) {}
String findOrder(OrderId id) => 'Order #${id.value}';
void main() {
print(findOrder(UserId(7)));
}
hello.dart:7:19: Error: The argument type 'UserId' can't be assigned to the parameter type 'OrderId'.
print(order) prints 42 — at runtime the value really is just an
int, so no extra object is created.
Adding a focused API
Extension types can add methods and hide the underlying type's members:
extension type Celsius(double degrees) {
Celsius operator +(Celsius other) => Celsius(degrees + other.degrees);
double get fahrenheit => degrees * 9 / 5 + 32;
bool get isFreezing => degrees <= 0;
}
void main() {
final today = Celsius(21.5);
final warmer = today + Celsius(3);
print(warmer.degrees);
print(warmer.fahrenheit);
print(Celsius(-2).isFreezing);
}
24.5
76.1
true
A Celsius exposes only what makes sense for temperatures — you can't
accidentally call .round() or add a plain number.
Extension types vs classes
- An extension type is purely a compile-time layer. Checks like
value is Celsiuslook at the underlying type at runtime, so they can't tell aCelsiusfrom any otherdouble. - A class creates a real object, with its own runtime identity.
Use extension types for zero-cost wrappers — IDs, units, and typed views over JSON maps — and classes for everything else. They're also central to Dart's modern JavaScript interop.