Extension methods and extension types

Dart · Advanced · 12 min

Extensions keep helper code readable, and extension types give type safety for IDs and interop without allocation.

An extension adds members to a type you don't control:

extension StringCase on String {
  String get capitalized => isEmpty ? this : this[0].toUpperCase() + substring(1);
}
'dart'.capitalized; // Dart

Extensions can add methods, getters, setters and operators, and can be generic (extension on List<T>). They are resolved statically, based on the variable's static type: they don't work on dynamic values and can't override existing members. If two imported extensions conflict, hide one or call it explicitly: StringCase('x').capitalized. Flutter code often uses extensions such as context.theme helpers.

An extension type (Dart 3.3+) wraps an existing type with a new static interface and no run-time cost:

extension type UserId(int value) {
  bool get isValid => value > 0;
}

At compile time UserId is distinct from int, so you can't pass a raw int or an OrderId where a UserId is expected. At run time it is the int — no wrapper object is allocated. Add implements int to expose the representation type's members.

Because the wrapper is erased, is checks and casts see the underlying type, so extension types are not a security boundary. They shine for strongly typed IDs and units, and for JS interop (dart:js_interop is built on them).

Example

extension Stats on Iterable<num> {
  double get average => isEmpty ? 0 : fold<num>(0, (a, b) => a + b) / length;
}
extension DurationFormat on Duration {
  String get mmss =>
      '${inMinutes.toString().padLeft(2, '0')}:${(inSeconds % 60).toString().padLeft(2, '0')}';
}

extension type UserId(int value) {
  bool get isValid => value > 0;
}
extension type OrderId(int value) {}

String loadUser(UserId id) => id.isValid ? 'user #${id.value}' : 'invalid';

void main() {
  print([2, 4, 9].average);                       // 5.0
  print(const Duration(seconds: 125).mmss);       // 02:05

  final id = UserId(42);
  print(loadUser(id));                            // user #42
  // loadUser(OrderId(42));                       // compile error
  // loadUser(42);                                // compile error
  print((id as Object) is int);                   // true: erased at run time
}

Remember

  • Extensions add members, resolved by static type
  • Extension types are compile-time-only wrappers with zero run-time cost
  • Use extension types for typed IDs and interop, not for security

Watch out for

  • Calling an extension method on a dynamic value and getting a NoSuchMethodError.

Check yourself

  1. At run time, what is a value of `extension type UserId(int value)`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: dart.dev/language/extension-types

Builds on

Unlocks

Nothing builds on this yet.

Extension methods and extension types · Dart · Topolearn