Extension Methods and Extension Types
17.1 Extension Methods
Extensions are statically resolved (based on the static type). They don't work on dynamic values. Resolve conflicts with StringX('x').capitalize() or hide/show imports.
extension StringX on String {
bool get isValidEmail => RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$').hasMatch(this);
String capitalize() => isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
String truncate(int max) => length <= max ? this : '${substring(0, max)}…';
}
extension NumX on num {
Duration get seconds => Duration(milliseconds: (this * 1000).round());
}
extension ListSum on Iterable<num> {
num get total => fold(0, (a, b) => a + b);
}
extension<T> on List<T> { // unnamed (private to the file)
T? get secondOrNull => length > 1 ? this[1] : null;
}
void main() {
print('[email protected]'.isValidEmail);
print('dart'.capitalize());
print(2.5.seconds);
print([1, 2, 3].total);
}17.2 Extension Types (Dart 3.3)
Uses: type-safe IDs and units, wrapping JS interop types (package:web uses extension types), zero-overhead API views over existing types.
Unlike classes, extension types have no runtime representation: is/as checks see the underlying type.
extension type UserId(int value) { // zero-cost wrapper (erased at runtime)
UserId.parse(String s) : value = int.parse(s);
bool get isValid => value > 0;
}
extension type Email._(String value) implements String { // can expose the underlying type's members
factory Email(String raw) {
if (!raw.contains('@')) throw FormatException('Invalid email: $raw');
return Email._(raw.toLowerCase());
}
}
void sendTo(Email email) => print('Sending to $email');
void main() {
final id = UserId(42);
print(id.isValid);
// int raw = id; // ❌ not assignable without .value (unless it implements int)
final email = Email('[email protected]');
sendTo(email);
print(email.length); // String members available via `implements String`
// sendTo('[email protected]'); // ❌ compile error: type safety without runtime cost
}