Extensions in Dart — Adding Methods to Any Type
Extension methods let us add functionality to existing types — even ones we don't control. Here's how they work and when they're the right tool.
The problem extensions solve
We've all been here. You're working with a String and you need a method that doesn't exist. Maybe you want to check if it's a valid email. Maybe you want to capitalise the first letter. Maybe you want to parse it as JSON.
// What we want to write
var email = 'test@example.com';
if (email.isValidEmail) { ... }
// What we have to write without extensions
if (isValidEmail(email)) { ... }
The second version works, but it feels backwards. The data comes first, then the operation. With methods, we write email.isValidEmail — the noun, then the verb. It reads naturally.
But we can't modify the
String class. It's part of Dart's core library. We don't own it.
Extension methods solve this. They let us add methods to any type — even types we don't control — without modifying the original class or creating a wrapper.
Extension syntax
An extension is a named block that adds members to an existing type.
extension StringValidation on String {
bool get isValidEmail {
return contains('@') && contains('.');
}
bool get isNumeric {
return double.tryParse(this) != null;
}
}
void main() {
print('test@example.com'.isValidEmail); // true
print('42'.isNumeric); // true
print('hello'.isNumeric); // false
}
The structure is: extension Name on Type { members }. Inside the extension, this refers to the instance — the string we're calling the method on.
Extension methods — work just like regular methods.
extension StringHelpers on String {
String truncate(int maxLength) {
if (length <= maxLength) return this;
return '\${substring(0, maxLength)}...';
}
String repeat(int times) {
return List.filled(times, this).join();
}
}
'Hello World'.truncate(5); // 'Hello...'
'ha'.repeat(3); // 'hahaha'
Extension getters and setters — add computed properties.
extension StringCasing on String {
String get capitalised {
if (isEmpty) return this;
return '\${this[0].toUpperCase()}\${substring(1)}';
}
String get reversed {
return split('').reversed.join();
}
}
'hello'.capitalised; // 'Hello'
'dart'.reversed; // 'trad'
Extension operators — even operators can be extended.
extension NumericString on String {
String operator *(int times) {
return List.filled(times, this).join();
}
}
'ha' * 3; // 'hahaha'
Generic extensions
Extensions can be generic, adding methods to parameterised types like List<T> or Future<T>.
extension ListHelpers<T> on List<T> {
T? get firstOrNull => isEmpty ? null : first;
T? get lastOrNull => isEmpty ? null : last;
List<T> takeWhileInclusive(bool Function(T) test) {
var result = <T>[];
for (var item in this) {
result.add(item);
if (!test(item)) break;
}
return result;
}
}
var numbers = [1, 2, 3, 4, 5];
numbers.firstOrNull; // 1
<int>[].firstOrNull; // null
numbers.takeWhileInclusive((n) => n < 3); // [1, 2, 3]
Constrained generics — require the type parameter to implement something.
extension ComparableList<T extends Comparable<T>> on List<T> {
T? get maxOrNull {
if (isEmpty) return null;
return reduce((a, b) => a.compareTo(b) > 0 ? a : b);
}
T? get minOrNull {
if (isEmpty) return null;
return reduce((a, b) => a.compareTo(b) < 0 ? a : b);
}
}
[3, 1, 4, 1, 5].maxOrNull; // 5
['z', 'a', 'm'].minOrNull; // 'a'
The constraint T extends Comparable<T> ensures compareTo is available. Without it, the extension couldn't call comparison methods.
How extension methods are resolved
Extension methods are resolved statically — at compile time, based on the declared type, not the runtime type.
extension on int {
String get description => 'an integer';
}
extension on num {
String get description => 'a number';
}
int x = 42;
num y = 42;
print(x.description); // 'an integer'
print(y.description); // 'a number' — even though it's really an int
The variable y is declared as num, so it uses the num extension — even though the actual value is an int.
This means extension methods cannot be overridden. If you need polymorphic behaviour, use instance methods or mixins instead.
Explicit extension application. When multiple extensions define the same member, you can disambiguate:
extension A on String {
String get decorated => '*** \$this ***';
}
extension B on String {
String get decorated => '--- \$this ---';
}
// Ambiguous — won't compile
// 'hello'.decorated;
// Explicit — works
A('hello').decorated; // '*** hello ***'
B('hello').decorated; // '--- hello ---'
Unnamed and private extensions
Extensions can be unnamed. This is useful for one-off helpers that don't need to be referenced elsewhere.
extension on String {
bool get isBlank => trim().isEmpty;
}
' '.isBlank; // true
Unnamed extensions cannot be explicitly applied — there's no name to use. If there's a conflict, you can't disambiguate.
Private extensions — prefix the name with underscore.
extension _InternalHelpers on String {
String get _processed => trim().toLowerCase();
}
// Only usable within this file
var clean = input._processed;
Private extensions are useful for internal implementation helpers that shouldn't leak into the public API.
Exporting extensions. Named extensions can be exported from libraries, making them available to consumers.
// In string_extensions.dart
extension StringExtensions on String {
String get capitalised => ...;
}
// In another file
import 'string_extensions.dart';
'hello'.capitalised; // works because extension is imported
Extensions are only available when they're in scope. Import the file, get the methods. Don't import it, they don't exist.
Practical extension patterns
Extensions shine for specific use cases. Here are patterns that work well.
Pattern 1: Fluent APIs on primitives
extension DurationHelpers on int {
Duration get milliseconds => Duration(milliseconds: this);
Duration get seconds => Duration(seconds: this);
Duration get minutes => Duration(minutes: this);
Duration get hours => Duration(hours: this);
}
await Future.delayed(500.milliseconds);
var timeout = 30.seconds;
var workday = 8.hours;
Pattern 2: Safe parsing
extension SafeParsing on String {
int? toIntOrNull() => int.tryParse(this);
double? toDoubleOrNull() => double.tryParse(this);
DateTime? toDateTimeOrNull() => DateTime.tryParse(this);
}
var age = '25'.toIntOrNull() ?? 0;
var price = 'invalid'.toDoubleOrNull(); // null
Pattern 3: Collection conveniences
extension IterableHelpers<T> on Iterable<T> {
Map<K, List<T>> groupBy<K>(K Function(T) keyFn) {
var result = <K, List<T>>{};
for (var item in this) {
var key = keyFn(item);
(result[key] ??= []).add(item);
}
return result;
}
T? firstWhereOrNull(bool Function(T) test) {
for (var item in this) {
if (test(item)) return item;
}
return null;
}
}
var users = [User('Alice', 25), User('Bob', 25), User('Carol', 30)];
var byAge = users.groupBy((u) => u.age);
// {25: [Alice, Bob], 30: [Carol]}
Pattern 4: Null-safe operations
extension NullableString on String? {
bool get isNullOrEmpty => this == null || this!.isEmpty;
bool get isNullOrBlank => this == null || this!.trim().isEmpty;
String orEmpty() => this ?? '';
}
String? name;
if (name.isNullOrBlank) {
name = 'Anonymous';
}
Limitations and when not to use extensions
Extensions are powerful but not universal. Know their limits.
Don't use extensions when:
1. You need state. Extensions can't add fields. If you need to store data per instance, use a mixin or wrapper class.
// Won't work — extensions can't have instance fields
extension on User {
int loginCount = 0; // ERROR
}
2. You need polymorphism. Extension methods use static dispatch. If you need different behaviour based on runtime type, use instance methods.
extension on Animal {
void feed() => print('feeding animal');
}
extension on Dog {
void feed() => print('feeding dog');
}
Animal pet = Dog();
pet.feed(); // 'feeding animal' — not what we wanted
3. The method is core to the type's identity. If every user of the type needs this method, it probably belongs on the class itself, not an extension.
4. You're adding too many. Twenty extension methods on String is a code smell. Consider whether a dedicated class would be clearer.
Test your understanding
7 questions
Seven questions covering extension methods, resolution, and when to use them.