DEV Community

kanta13jp1
kanta13jp1

Posted on

Dart Extension Types — Zero-cost Type-safe Wrappers

Dart Extension Types — Zero-cost Type-safe Wrappers

Introduced in Dart 3.3, Extension Types let you treat an existing type as a distinct type at compile time — with zero runtime overhead. They're the "newtype" pattern Dart developers have wanted for years.

Basic Syntax

// Before: String IDs are easy to confuse
String userId = 'abc-123';
String orgId  = 'org-456';
// Passing orgId where userId is expected compiles fine ❌

// After: distinct types at compile time
extension type UserId(String value) implements String {}
extension type OrgId(String value) implements String {}

UserId userId = UserId('abc-123');
OrgId  orgId  = OrgId('org-456');

void fetchUser(UserId id) { ... }
fetchUser(orgId); // ❌ Compile error: OrgId is not UserId
Enter fullscreen mode Exit fullscreen mode

implements vs. No implements

// With implements String: all String methods are available
extension type UserId(String value) implements String {}

final id = UserId('abc-123');
id.length;           // ✅
id.toUpperCase();    // ✅
id.startsWith('a'); // ✅

// Without implements: only explicitly declared members
extension type StrictId(String _v) {
  String get value => _v;
  // .length, .toUpperCase() → compile error
}
Enter fullscreen mode Exit fullscreen mode

Adding Methods

extension type UserId(String value) implements String {
  bool get isValid => value.isNotEmpty && value.length <= 36;

  factory UserId.generate() =>
      UserId(DateTime.now().millisecondsSinceEpoch.toString());

  String toApiParam() => Uri.encodeComponent(value);
}

final id = UserId.generate();
if (id.isValid) {
  final url = '/api/users/${id.toApiParam()}';
}
Enter fullscreen mode Exit fullscreen mode

Design Tokens: Type-safe Colors

extension type AppColor(Color value) implements Color {
  static const AppColor primary   = AppColor(Color(0xFF6366F1));
  static const AppColor secondary = AppColor(Color(0xFFF97316));
  static const AppColor surface   = AppColor(Color(0xFF1E1B4B));

  AppColor withAlpha50() => AppColor(value.withAlpha(128));
}

Widget buildButton(AppColor color) => ElevatedButton(
  style: ElevatedButton.styleFrom(backgroundColor: color),
  onPressed: () {},
  child: const Text('Click'),
);

buildButton(AppColor.primary); // ✅
buildButton(Colors.blue);      // ❌ Compile error
Enter fullscreen mode Exit fullscreen mode

Supabase: Type-safe ID Management

extension type TaskId(String value) implements String {
  factory TaskId.fromJson(dynamic json) => TaskId(json as String);
}
extension type UserId(String value) implements String {
  factory UserId.fromJson(dynamic json) => UserId(json as String);
}

class Task {
  final TaskId id;
  final UserId userId;
  final String title;

  factory Task.fromJson(Map<String, dynamic> json) => Task(
    id:     TaskId.fromJson(json['id']),
    userId: UserId.fromJson(json['user_id']),
    title:  json['title'] as String,
  );
}

// Impossible to accidentally pass a UserId where TaskId is expected
Future<Task?> fetchTask(TaskId id) async {
  final res = await supabase
      .from('tasks')
      .select()
      .eq('id', id.value)
      .maybeSingle();
  return res != null ? Task.fromJson(res) : null;
}
Enter fullscreen mode Exit fullscreen mode

Extension Types vs. typedef

// typedef: just an alias — no type safety
typedef UserId = String;
void fetchUser(UserId id) {}
fetchUser('org-456'); // compiles ✅ (useless)

// Extension Type: actually distinct
extension type UserId(String value) implements String {}
void fetchUser(UserId id) {}
fetchUser('org-456'); // ❌ Compile error
Enter fullscreen mode Exit fullscreen mode

When to Use implements

Use case Recommendation
Safe wrapper for existing type implements
Completely isolated new type No implements
Validated string (email, URL) implements String
Units (km, m, cm — prevent mixing) No implements

Since adopting Extension Types for all ID fields, we've had zero "wrong ID type" bugs in production.


How are you using Extension Types in your codebase? Let me know below!

Top comments (0)