Chuyển đến nội dung chính

Extension types in Dart: a new name for an old value

Every codebase past a certain size has this bug at least once:

void transfer(String fromUserId, String toAccountId, int cents) { ... }

transfer(accountId, userId, 500); // compiles fine, wrong at runtime

Both are String, so the type system has nothing to say. The usual fix is a wrapper class, which costs an allocation on every ID you touch. Extension types are the same fix without the allocation.

extension type UserId(String value) {}
extension type AccountId(String value) {}

void transfer(UserId from, AccountId to, int cents) { ... }

transfer(accountId, userId, 500); // compile error

At runtime, UserId is a String. There is no wrapper object, no field access indirection, nothing allocated. The distinction exists only in the static type system, and is erased before the program runs.

What “erased” actually means

This is the part that decides whether extension types fit your problem. The compiler replaces the extension type with its representation type. Consequences follow directly:

extension type UserId(String value) {}

final id = UserId('u_123');

print(id is String);       // true
print(id.runtimeType);     // String
print(id is UserId);       // this is a compile-time check, not a runtime one

// And crucially:
final list = <Object>[UserId('a'), 'a'];
print(list[0] == list[1]); // true — both are the string 'a'

So an extension type gives you compile-time distinctness only. If your code branches on runtimeType, stores values in a heterogeneous list and switches on their type, or relies on is checks at runtime to tell IDs apart, extension types will not do it. A wrapper class will.

A second consequence: extension types are not subtypes in the usual sense, and you cannot make one implement an interface unless the representation type does. extension type UserId(String value) implements Comparable<UserId> will not compile just because you’d like it to.

Controlling the surface

By default an extension type exposes nothing from its representation type:

extension type UserId(String value) {}

final id = UserId('u_123');
id.length;        // compile error — String members are not inherited
id.value.length;  // fine

That is usually what you want for an ID: UserId is not a string you should be uppercasing. When you do want the underlying API, declare it explicitly:

extension type Meters(double value) implements Comparable<num> {
  Meters operator +(Meters other) => Meters(value + other.value);
  Meters operator *(double k) => Meters(value * k);
  double get inFeet => value * 3.28084;

  @override
  int compareTo(num other) => value.compareTo(other);
}

final total = Meters(4.5) + Meters(2.0);
print(total.inFeet);

implements here does not mean subclassing — it means “also allow these members through, and treat this type as assignable to that one.” Note that implements num would make Meters freely assignable to num, which throws away the safety you added; be sparing with what you expose.

The comparison to alternatives:

ApproachRuntime costDistinct at compile timeDistinct at runtimeCan add methods
typedef UserId = Stringnonenonono
extension on Stringnonenonoyes (on all Strings)
extension type UserId(String)noneyesnoyes
class UserId { final String v; }allocationyesyesyes

The row worth staring at is the second: an extension method adds userIdish behaviour to every String in your program. Extension types add it to one named type.

The interop case

Extension types were designed alongside dart:js_interop, and that is where they are least optional. A JavaScript object arriving in Dart has no Dart class; modelling it as an extension type over JSObject gives you a typed API with no marshalling cost:

extension type DomRect._(JSObject _) implements JSObject {
  external double get width;
  external double get height;
}

The ._ names a private constructor, which is the idiom for “this value comes from somewhere else, do not construct it yourself.”

Where I use them, and where I don’t

Use them for:

  • IDs and opaque handles. UserId, SessionToken, Sku. The classic case.
  • Units. Meters, Cents, Milliseconds. Mixing units is a real bug class, and an allocation per value is a real cost in hot paths.
  • Validated strings where validation happens once at the boundary — Email, Slug — via a factory that throws or returns null.
  • Interop wrappers, as above.

Do not use them for:

  • Anything you need to is-check at runtime.
  • Anything stored heterogeneously and dispatched on type.
  • Domain models. A User should be a class; it has invariants an extension type cannot enforce and identity semantics an extension type cannot give.
  • Values you serialise with a code generator that reflects over runtime types — the generator sees String, not UserId.

The honest summary is that extension types solve a narrow problem completely. They are not a lighter class; they are a static-only naming layer with an escape hatch. Reaching for them where you actually wanted a class produces code that compiles beautifully and behaves surprisingly.

A validated example, since it’s the pattern most worth copying:

extension type const Email._(String value) {
  static final _re = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$');

  factory Email(String raw) {
    final trimmed = raw.trim();
    if (!_re.hasMatch(trimmed)) {
      throw FormatException('Not an email: $raw');
    }
    return Email._(trimmed);
  }

  static Email? tryParse(String raw) =>
      _re.hasMatch(raw.trim()) ? Email._(raw.trim()) : null;
}

Now a function taking Email has a static guarantee that validation already ran, at zero runtime cost, and there is exactly one place in the codebase where that validation lives.

FAQ

Are extension types the same as Kotlin’s value classes?

Similar intent, different mechanics. Kotlin’s inline classes can box in some situations; Dart’s extension types never exist at runtime at all.

Can I use one as a map key?

Yes, and it behaves as the representation type does — UserId('a') and the plain string 'a' are the same key. That is occasionally convenient and occasionally a bug.

Do they work with const?

Yes: extension type const Email._(String value) allows const construction where the representation is const.

Can two extension types over the same representation be assigned to each other?

Not directly — that is the entire point. Convert explicitly through .value.

Should I add implements to get the underlying methods?

Only for the ones you actually want. Every member you expose is a way for the distinction to leak away.


Erasure semantics, the default member surface, implements behaviour, and the dart:js_interop usage described here are documented in the Dart references linked above. The use/don’t-use lists, the comparison table’s practical readings, and the validated-Email pattern are my own judgement from applying extension types in production code. Verify behaviour against the Dart SDK version your project targets.


Originally published on FlutterCook. Read the latest version there — that copy is the one kept up to date.

Nhận xét

Bài đăng phổ biến từ blog này

5 concepts every Flutter dev should know

  Phụ lục: State management architecture Testing IDE Shortcuts Platform channel Maintaining a project Tôi đã làm việc với Flagship trong một thời gian dài, và đây là những điều mà tôi phát hiện ra là điều cần phải có đối với bất kỳ nhà phát triển Flagship nào, về tổng thể nó sẽ khiến bạn trở thành một nhà phát triển Flagship giỏi trong thời gian dài. 1. State management architecture Đây là một trong những chủ đề quan trọng nhất trong cộng đồng thiết bị rung, nó khá quan trọng nếu bạn muốn duy trì một dự án rung kích thước trung bình hoặc lớn. Nó sẽ giúp tạo một dự án suôn sẻ và thêm các tính năng mới một cách hoàn hảo.  2. Testing Đây là một chủ đề duy nhất mà tôi không hiểu tại sao nó lại quan trọng trước đó trong sự nghiệp của tôi, nhưng khi tôi tiến lên trong sự nghiệp của mình và có kinh nghiệm với nhiều dự án và vấn đề xảy ra trong môi trường sản xuất. Tôi đã nhận ra một cách khó khăn, tại sao điều này lại quan trọng như vậy. Nếu bạn vẫn muốn có thêm lý do để cân nhắc thử...

Thiết kế giao diện với DotNetBar (Phần 1)

Đây là phiên bản DotNetBar hỗ trợ C# và Visual Basic https://www.dropbox.com/s/wx80jpvgnlrmtux/DotNetBar.rar  , phiên bản này hỗ trợ giao diện Metro cực kỳ “dễ thương” Các bạn load về và cài đặt, khi cài đặt xong sẽ có source code mẫu của tất cả các control. Để sử dụng được các control của DotNetBar các bạn nhớ add item vào controls box. Thiết kế giao diện với DotNetBar, giao diện sẽ rất đẹp. Link các video hướng dẫn chi tiết cách sử dụng và coding: http://www.devcomponents.com/dotnetbar/movies.aspx Hiện tại DotNetBar có rất nhiều công cụ cực mạnh, trong đó có 3 công cụ dưới đây: DotNetBar for Windows Forms Requires with Visual Studio 2003, 2005, 2008, 2010 or 2012.   DotNetBar for WPF Requires with Visual Studio 2010 or 2012 and Windows Presentation Foundation.   DotNetBar for Silverlight Requires with Visual Studio 2010 or 2012 and Silverlight. Dưới đây là một số hình ảnh về các control trong DotnetBar.   Metro User Interface  controls with Metro Tiles, toolba...

Announcing Flutter 2

  Phụ lục: Flutter on the web Flutter 2 on desktops, foldables, and embedded devices The growing Flutter ecosystem Dart: The secret sauce behind Flutter Flutter 2: Available now Hôm nay, chúng tôi sẽ công bố Flutter 2: một bản nâng cấp lớn cho Flutter cho phép các nhà phát triển tạo các ứng dụng đẹp, nhanh chóng và di động cho bất kỳ nền tảng nào. Với Flutter 2, bạn có thể sử dụng cùng một cơ sở mã để gửi các ứng dụng gốc cho năm hệ điều hành: IOS, Android, Windows, macOS và Linux; cũng như trải nghiệm web nhắm mục tiêu các trình duyệt như Chrome, Firefox, Safari hoặc Edge. Flutter thậm chí có thể được nhúng vào ô tô, TV và thiết bị gia dụng thông minh, mang đến trải nghiệm di động và lan tỏa nhất cho thế giới điện toán xung quanh. Mục tiêu của chúng tôi là thay đổi cơ bản cách các nhà phát triển nghĩ về việc xây dựng ứng dụng, bắt đầu không phải với nền tảng bạn đang nhắm mục tiêu mà là với trải nghiệm bạn muốn tạo. Flutter cho phép bạn tạo ra những trải nghiệm tuyệt đẹp trong đó ...