What are extension methods and when would you use them?
3 min read
Dart Fundamentals
| Aspect | Extension method | Subclass / wrapper |
|---|---|---|
| Modifies the original type? | ❌ No (purely additive at call site) | ❌ No |
| Polymorphic / dynamic dispatch? | ❌ Static — resolved by declared type | ✅ Yes |
| Works on types you don't own? | ✅ Yes (String, int, BuildContext) | ✅ Usually |
| Cost at runtime | Zero — just a function call | Allocation + indirection |
The key gotcha: extensions are statically dispatched. They use the declared type of the variable, not the runtime type.
Code in action
extension StringX on String {
String get capitalized =>
isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
String truncate(int max) =>
length <= max ? this : '${substring(0, max)}…';
bool get isEmail =>
RegExp(r'^[\w.+-]+@\w+(\.\w+)+$').hasMatch(this);
}
'alice'.capitalized; // 'Alice'
'Hello world'.truncate(5); // 'Hello…'
'nik@x.com'.isEmail; // true
// Generic extension — works on any List<T>
extension ListX<T> on List<T> {
T? get firstOrNull => isEmpty ? null : first;
List<T> get unique => {...this}.toList();
}
[1, 2, 2, 3].unique; // [1, 2, 3]
<int>[].firstOrNull; // null — no crash
The killer use case in Flutter: BuildContext shortcuts
extension BuildContextX on BuildContext {
ThemeData get theme => Theme.of(this);
TextTheme get textTheme => theme.textTheme;
ColorScheme get colors => theme.colorScheme;
Size get screenSize => MediaQuery.sizeOf(this);
void snack(String msg) =>
ScaffoldMessenger.of(this).showSnackBar(SnackBar(content: Text(msg)));
Future<T?> push<T>(Widget page) => Navigator.of(this)
.push<T>(MaterialPageRoute(builder: (_) => page));
}
// In a widget:
Container(
color: context.colors.primary,
width: context.screenSize.width / 2,
child: Text('Hi', style: context.textTheme.headlineSmall),
);
The boilerplate of Theme.of(context) / Navigator.of(context) disappears.
When to reach for an extension
| Situation | Use extension? |
|---|---|
Add a helper to a type you don't own (String, DateTime, BuildContext) | ✅ Yes |
| Group related sugar around a domain type (formatters, validators) | ✅ Yes |
| Behaviour that depends on runtime type / subtype | ❌ Use methods or visitor — extensions dispatch statically |
| Replace a real class (DTO, value object) | ❌ Make a proper class |
| State or fields | ❌ Extensions can't add fields — only methods, getters, operators |
Common mistakes to avoid
// ❌ Forgetting extensions are statically dispatched
extension on Animal { String label() => 'animal'; }
extension on Dog { String label() => 'dog'; }
Animal a = Dog();
a.label(); // 'animal' ← uses DECLARED type Animal, not Dog!
// ✅ For polymorphic behaviour, use real methods, not extensions.
// ❌ Dumping unrelated helpers in one extension
extension DateTimeStuff on DateTime {
String get formatted => ...;
double calculateMortgage() => ...; // 🤔 belongs nowhere near DateTime
}
// ❌ Forgetting to import the extension
// Extensions only work when their library is imported — silent failure
// where the method "doesn't exist" until you add the import.
// ❌ Trying to add a field
extension UserX on User {
late String _cache; // ❌ extensions can't hold state
}
// ❌ Two extensions defining the same method on the same type
// Resolution becomes ambiguous — Dart will require you to disambiguate with
// ExtensionName(value).method()
Interview follow-ups
-
Are extension methods resolved at compile time or runtime? Compile time. The compiler picks the extension based on the static (declared) type of the receiver. That's why
Animal a = Dog(); a.label();calls theAnimalextension, not theDogone — a common gotcha. -
Can extensions add fields or state? No. Only methods, getters/setters, and operators. Fields would require modifying the class's memory layout, which extensions deliberately don't touch.
-
What happens if two imported extensions define the same method on the same type? Dart raises an ambiguity error at the call site. You resolve it explicitly:
ExtensionName(value).method()or by hiding one extension during import. -
When would you prefer a static helper function over an extension? When the function doesn't logically "belong" to the type (e.g.,
parseUser(json)doesn't belong onMap), when you want to keep the type's surface small, or when polymorphic dispatch matters — then prefer a real method on an interface.
How helpful was this content?
Please sign in to rate this article.