flutter
/

Dart Extension Types – Wrapping Types with Zero‑Cost Abstractions

Last Sync: Today

On this page

11
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Dart Extension Types – Wrapping Types with Zero‑Cost Abstractions

What are Extension Types?

Introduced in Dart 3.3, extension types allow you to create a compile‑time wrapper around an existing type. They are similar to extension methods but provide a new type that is distinct at compile time while having zero runtime overhead. Extension types are ideal for adding type safety to primitive types (like wrapping String with EmailAddress) or for creating domain‑specific types without the cost of creating a full class.

Why Use Extension Types?

    • Type safety – prevent accidental mixing of conceptually different values of the same underlying type (e.g., Email vs Name).
    • Zero runtime cost – no extra object allocation; the wrapper exists only at compile time.
    • Clearer API – self‑documenting types make interfaces more expressive.
    • Opt‑in to existing methods – you can expose or hide methods of the underlying type.

Syntax of Extension Types

An extension type is declared using extension type followed by a name and the underlying type in parentheses. It can define methods, getters, and even new constructors.

DARTRead-only
1
extension type EmailAddress(String value) {
  // Validation in constructor
  EmailAddress(this.value) {
    if (!value.contains('@')) {
      throw ArgumentError('Invalid email');
    }
  }

  // Add domain getter
  String get domain => value.split('@')[1];

  // Add method
  bool isValid() => value.contains('@');
}

void main() {
  var email = EmailAddress('user@example.com');
  print(email.domain); // example.com
  // email is of type EmailAddress, not String
  // String s = email; // Error: can't assign EmailAddress to String
}

Underlying Representation

The extension type wraps the underlying value but does not create a new object at runtime. The wrapper is just a compile‑time type; the underlying representation is the same as the original type. This means extension types are zero‑cost – they don't allocate extra memory and have no performance penalty.

DARTRead-only
1
extension type Meters(double value) {}
extension type Feet(double value) {}

void main() {
  Meters distance = Meters(10.0);
  // At runtime, distance is just a double
  // No extra object is created
}

Exposing Underlying Members

By default, the wrapper does not expose any members of the underlying type. You must explicitly forward methods or properties you want to expose. This gives you full control over the public API.

DARTRead-only
1
extension type PositiveInt(int value) {
  // Explicitly forward the `toString` method
  @override
  String toString() => value.toString();

  // Forward the `abs` method (but since it's positive, it's the same)
  int abs() => value.abs();

  // Do NOT expose `isEven` – we want to hide it
}

Constructors and Factories

Extension types can have constructors, including redirecting constructors and factory constructors. They can also perform validation during construction.

DARTRead-only
1
extension type Age(int value) {
  Age(this.value) {
    if (value < 0 || value > 120) {
      throw ArgumentError('Age must be between 0 and 120');
    }
  }

  // Redirecting constructor
  Age.min() : this(0);

  // Factory constructor
  factory Age.fromString(String s) => Age(int.parse(s));
}

Implicit Exposure (Shortcut)

You can use the on keyword to automatically expose all members of the underlying type. This is a shortcut when you want the extension type to behave like the original type but with a different type name.

DARTRead-only
1
extension type Kilometers(num value) on num {
  // All `num` methods are automatically available
}

void main() {
  Kilometers km = Kilometers(5);
  double converted = km + 3; // works because `+` is forwarded
  print(km.isFinite); // true
}

Extension Types vs Regular Extensions

    • Regular extension adds methods to an existing type (like String). It does not create a new type.
    • Extension type creates a new type that wraps an existing type. It's a distinct type at compile time, with zero runtime overhead.

Use Cases

    • Domain‑specific types – EmailAddress, UserId, ProductId.
    • Unit conversion – Meters, Feet, Celsius, Fahrenheit.
    • Validation wrappers – ensure valid values at construction.
    • API boundaries – enforce correct types without runtime cost.

Complete Example

DARTRead-only
1
extension type Celsius(double value) {
  Celsius(this.value);

  Celsius.toFahrenheit(double fahrenheit) : value = (fahrenheit - 32) * 5 / 9;

  double get fahrenheit => value * 9 / 5 + 32;

  @override
  String toString() => '${value.toStringAsFixed(1)}°C';
}

extension type Fahrenheit(double value) {
  Fahrenheit(this.value);

  double get celsius => (value - 32) * 5 / 9;

  @override
  String toString() => '${value.toStringAsFixed(1)}°F';
}

void main() {
  var temp = Celsius(25);
  print(temp);              // 25.0°C
  print(temp.fahrenheit);   // 77.0

  var f = Fahrenheit(77);
  print(f.celsius);         // 25.0
}

Key Takeaways

    • Extension types create new types with zero runtime overhead.
    • They are defined with extension type Name(UnderlyingType representation).
    • Use them to add type safety to primitive types, perform validation, and hide implementation details.
    • They have constructors, can expose or hide underlying members, and can be used in on clauses to automatically forward all members.
    • Ideal for domain modelling, units, and ensuring correct usage at API boundaries.

Try it yourself

extension type Email(String value) {
  Email(this.value) {
    if (!value.contains('@')) {
      throw ArgumentError('Invalid email');
    }
  }
  String get domain => value.split('@')[1];
}

void main() {
  try {
    var email = Email('user@example.com');
    print('Domain: ${email.domain}');

    // This will throw
    var invalid = Email('not-an-email');
  } catch (e) {
    print('Error: $e');
  }
}

Test Your Knowledge

Q1
of 4

What is the main advantage of extension types over regular classes?

A
They are easier to write
B
They have zero runtime overhead
C
They support multiple inheritance
D
They are automatically immutable
Q2
of 4

How do you expose all members of the underlying type in an extension type?

A
Use the `expose` keyword
B
Use the `on` clause
C
Write forwarding methods manually
D
It's impossible
Q3
of 4

What happens at runtime when you create an extension type instance?

A
A new object is allocated
B
No new object is allocated; only the underlying value exists
C
A new object is allocated but immediately garbage collected
D
The program crashes
Q4
of 4

Which Dart version introduced extension types?

A
Dart 2.19
B
Dart 3.0
C
Dart 3.3
D
Dart 3.5

Frequently Asked Questions

Are extension types available in all Dart versions?

No, extension types were introduced in Dart 3.3. You need Dart 3.3 or later to use them.

What is the runtime cost of extension types?

Extension types have zero runtime overhead. They are a compile‑time abstraction; at runtime, the value is exactly the underlying type (e.g., a String). No extra objects are created.

How do extension types differ from ordinary classes?

A class creates a new runtime object; an extension type does not – it's purely a compile‑time wrapper. Classes can have multiple fields and methods; extension types only have the single underlying field (the representation).

Can I use extension types with generics?

Yes, you can have generic extension types. For example: extension type Pair<T>(T value).

Can I create extension types for nullable types?

Yes, you can have extension type NullableString(String? value). The underlying type can be nullable.

How do I compare two extension type instances?

By default, they compare based on the underlying value. You can override == and hashCode if you need custom equality.

Previous

dart patterns

Next

dart enums

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.