Skip to content

Extension Types

Understand how to create zero-cost wrappers around existing types in Dart.


What is it?

Extension Types are a powerful feature introduced in Dart 3 that allow you to create a compile-time wrapper around an existing type. Unlike regular extensions that add methods, extension types create a completely new type that behaves like the underlying type but with a different interface.


Why does it exist?

Extension Types exist to:

  • Create lightweight wrappers with zero runtime cost
  • Add type safety without runtime overhead
  • Define domain-specific types
  • Hide implementation details
  • Provide alternative APIs for existing types
  • Prevent accidental mixing of semantically different values

Basic Extension Types

Simple Extension Type

// Extension type wrapping String
extension type Email(String value) {
  // Validation
  bool get isValid => value.contains('@') && value.contains('.');

  // Getters
  String get domain => value.split('@')[1];
  String get username => value.split('@')[0];
}

// Usage
Email email = Email('user@example.com');
print(email.isValid); // true
print(email.domain); // example.com
print(email.username); // user

// Underlying value access
String rawEmail = email.value; // 'user@example.com'

Extension Type with Multiple Fields

// Extension type wrapping multiple primitive types
extension type Person((String name, int age) data) {
  String get name => data.$1;
  int get age => data.$2;

  bool get isAdult => age >= 18;

  String get greeting => 'Hello, I am $name, $age years old';
}

// Usage
var person = Person(('Alice', 25));
print(person.name); // Alice
print(person.age); // 25
print(person.isAdult); // true
print(person.greeting); // Hello, I am Alice, 25 years old

// Access underlying data
var raw = person.data; // ('Alice', 25)

Extension Types vs Regular Extensions

Extension Type (Compile-time Wrapper)

// Extension Type - Creates a new type
extension type Id(String value) {
  bool get isValid => value.isNotEmpty && RegExp(r'^[A-Z0-9]+$').hasMatch(value);
}

extension type UserId(String value) {
  bool get isValid => value.isNotEmpty && value.length >= 3;
}

void processId(Id id) {
  if (id.isValid) {
    print('Valid ID: ${id.value}');
  }
}

void processUserId(UserId userId) {
  if (userId.isValid) {
    print('Valid User ID: ${userId.value}');
  }
}

// Usage
Id id = Id('ABC123');
UserId userId = UserId('abc123');

processId(id);        // Valid ID: ABC123
processUserId(userId); // Valid User ID: abc123

// Type safety - prevents mixing
// processId(userId); // Error! Cannot assign UserId to Id

Domain-Specific Types

Creating Type-Safe Values

// Extension types for domain values
extension type ProductCode(String value) {
  // Validation logic
  bool get isValid => RegExp(r'^[A-Z]{2}\d{4}$').hasMatch(value);

  // Parsing
  String get category => value.substring(0, 2);
  String get number => value.substring(2);

  // Named constructors
  ProductCode.fromCategory(String category, String number) :
    this('${category.toUpperCase()}$number');
}

extension type Price(double value) {
  // Operations
  Price operator +(Price other) => Price(value + other.value);
  Price operator -(Price other) => Price(value - other.value);
  bool operator >(Price other) => value > other.value;
  bool operator <(Price other) => value < other.value;

  // Formatting
  String get formatted => '\$${value.toStringAsFixed(2)}';

  // Validation
  bool get isValid => value >= 0;
}

// Usage
var product = ProductCode('AB1234');
print(product.isValid); // true
print(product.category); // AB
print(product.number); // 1234

var price1 = Price(19.99);
var price2 = Price(9.99);
print((price1 + price2).formatted); // $29.98
print(price1 > price2); // true

Wrapping Collections

Extension Types with Collections

// Extension type wrapping a List
extension type UserList(List<String> users) {
  // Delegate methods
  int get length => users.length;
  bool get isEmpty => users.isEmpty;
  bool get isNotEmpty => users.isNotEmpty;

  // Custom operations
  bool contains(String user) => users.contains(user);
  String? get first => users.isNotEmpty ? users.first : null;
  String? get last => users.isNotEmpty ? users.last : null;

  // Modifying methods
  void add(String user) => users.add(user);
  void remove(String user) => users.remove(user);

  // Filtering
  List<String> get sorted => [...users]..sort();

  // Representation
  String get joined => users.join(', ');
}

// Usage
var list = UserList(['Alice', 'Bob', 'Charlie']);
print(list.length); // 3
print(list.joined); // Alice, Bob, Charlie
list.add('David');
print(list.joined); // Alice, Bob, Charlie, David

Extension Type with Map

extension type Settings(Map<String, dynamic> config) {
  // Safe getters
  String getString(String key, [String defaultValue = '']) =>
      config[key] as String? ?? defaultValue;

  int getInt(String key, [int defaultValue = 0]) =>
      config[key] as int? ?? defaultValue;

  bool getBool(String key, [bool defaultValue = false]) =>
      config[key] as bool? ?? defaultValue;

  // Setters
  void setString(String key, String value) => config[key] = value;
  void setInt(String key, int value) => config[key] = value;
  void setBool(String key, bool value) => config[key] = value;

  // Check
  bool containsKey(String key) => config.containsKey(key);
}

// Usage
var settings = Settings({
  'host': 'localhost',
  'port': 8080,
  'debug': true,
});

print(settings.getString('host')); // localhost
print(settings.getInt('port')); // 8080
print(settings.getBool('debug')); // true
print(settings.getString('unknown', 'default')); // default

Type Safety with Extension Types

Preventing Unit Confusion

// Extension types for different units
extension type Meters(double value) {
  // Convert to kilometers
  Kilometers get toKilometers => Kilometers(value / 1000);
  Centimeters get toCentimeters => Centimeters(value * 100);

  // Operations
  Meters operator +(Meters other) => Meters(value + other.value);
  Meters operator -(Meters other) => Meters(value - other.value);
  bool operator >(Meters other) => value > other.value;

  String get formatted => '${value}m';
}

extension type Kilometers(double value) {
  Meters get toMeters => Meters(value * 1000);
  Centimeters get toCentimeters => Centimeters(value * 100000);

  String get formatted => '${value}km';
}

extension type Centimeters(double value) {
  Meters get toMeters => Meters(value / 100);
  Kilometers get toKilometers => Kilometers(value / 100000);

  String get formatted => '${value}cm';
}

// Usage - Type safe!
Meters length1 = Meters(1000);
Kilometers length2 = length1.toKilometers; // 1km
Centimeters length3 = length2.toCentimeters; // 100000cm

// Cannot mix units directly
// Kilometers invalid = length1; // Error! Different types
// Centimeters invalid2 = length2; // Error! Different types

print(length1.formatted); // 1000m
print(length2.formatted); // 1km
print(length3.formatted); // 100000cm

Currency Types

extension type USD(double value) {
  USD operator +(USD other) => USD(value + other.value);
  USD operator -(USD other) => USD(value - other.value);

  Euro get toEuro => Euro(value * 0.85);
  GBP get toGBP => GBP(value * 0.72);

  String get formatted => '\$${value.toStringAsFixed(2)}';
}

extension type Euro(double value) {
  USD get toUSD => USD(value / 0.85);
  GBP get toGBP => GBP(value * 0.85);

  String get formatted => '€${value.toStringAsFixed(2)}';
}

extension type GBP(double value) {
  USD get toUSD => USD(value / 0.72);
  Euro get toEuro => Euro(value / 0.85);

  String get formatted => ${value.toStringAsFixed(2)}';
}

// Usage
var dollars = USD(100);
var euros = dollars.toEuro; // €85.00
var pounds = euros.toGBP; // £72.25

print(dollars.formatted); // $100.00
print(euros.formatted); // €85.00
print(pounds.formatted); // £72.25

Best Practices

Use for Domain Modeling

// Good: Domain-specific types
extension type Email(String value) {
  bool get isValid => value.contains('@') && value.contains('.');
}

extension type PhoneNumber(String value) {
  bool get isValid => RegExp(r'^\d{10}$').hasMatch(value);
}

extension type ZipCode(String value) {
  bool get isValid => RegExp(r'^\d{5}(-\d{4})?$').hasMatch(value);
}

// Usage with type safety
void sendEmail(Email email) {
  if (email.isValid) {
    print('Sending to ${email.value}');
  }
}

// sendEmail(PhoneNumber('1234567890')); // Error! Type mismatch

Use for API Boundaries

// Extension types for API data
extension type UserId(String value) {
  bool get isValid => value.isNotEmpty;
}

extension type ApiKey(String value) {
  bool get isValid => value.length >= 32;
}

class ApiClient {
  String? _apiKey;

  void setApiKey(ApiKey key) {
    if (key.isValid) {
      _apiKey = key.value;
    }
  }

  Future<void> getUser(UserId userId) async {
    if (!userId.isValid) {
      throw ArgumentError('Invalid user ID');
    }
    // Fetch user with userId.value
  }
}

Common Mistakes

Unnecessary Extension Types

Wrong:

// Overkill for simple cases
extension type SimpleString(String value) {
  // No extra behavior
}

Correct:

// Regular type alias if no behavior needed
typedef SimpleString = String;

// Or extension type with actual value
extension type ValidatedString(String value) {
  bool get isValid => value.isNotEmpty;
}

Exposing Too Much

Wrong:

extension type User((String name, int age) data) {
  String get name => data.$1;
  int get age => data.$2;
  // Exposing internal structure - Bad
}

Correct:

extension type User((String name, int age) data) {
  String get name => data.$1;
  int get age => data.$2;

  // Expose only what's needed
  bool get isAdult => age >= 18;
  String get greeting => 'Hello, $name';
}

Summary

Extension Types provide zero-cost wrappers that create new types with specific interfaces. They're ideal for domain modeling, adding type safety, and preventing accidental mixing of semantically different values.


Next Steps

Now that you understand extension types, continue to:


Did You Know?

  • Extension types were introduced in Dart 3
  • They have zero runtime cost (compile-time only)
  • Extension types wrap existing types
  • They can have methods, getters, and operators
  • Extension types provide type safety
  • They're great for domain modeling
  • Extension types are different from regular extensions