Extensions
Understand how to add functionality to existing types in Dart.
What is it?
Extensions allow you to add new methods, getters, setters, and operators to existing types without modifying their source code. This powerful feature enables you to extend classes, enums, and even primitive types with custom functionality.
Why does it exist?
Extensions exist to:
- Add functionality to types you don't own
- Keep code organized and modular
- Create more expressive APIs
- Add utility methods to existing classes
- Avoid utility classes (e.g., StringUtils)
- Provide domain-specific extensions
Basic Extensions
Simple Extension
// Extension on String
extension StringExtension on String {
// Add a method
String reverse() {
return split('').reversed.join('');
}
// Add a getter
bool get isPalindrome {
return this == reverse();
}
// Add a setter (rare)
// Not possible directly with simple extensions
}
// Usage
String text = 'hello';
print(text.reverse()); // 'olleh'
print('radar'.isPalindrome); // true
print('hello'.isPalindrome); // false
Extension with Parameters
extension NumberExtension on int {
// Method with parameters
int add(int other) => this + other;
int multiply(int other) => this * other;
// Multiple parameters
int calculate(int a, int b) => this * a + b;
// Generic method
T apply<T>(T Function(int) transform) => transform(this);
}
// Usage
int num = 5;
print(num.add(3)); // 8
print(num.multiply(2)); // 10
print(num.calculate(2, 3)); // 13
print(num.apply((n) => n * n)); // 25
Extension Properties
Getters and Setters
extension DateTimeExtension on DateTime {
// Getter
String get shortDate {
return '${year}-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
}
String get shortTime {
return '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}';
}
// Computed property
String get formatted {
return '$shortDate $shortTime';
}
// Boolean getter
bool get isWeekend {
return weekday == DateTime.saturday || weekday == DateTime.sunday;
}
bool get isWorkday => !isWeekend;
}
// Usage
final now = DateTime.now();
print(now.shortDate); // 2026-06-29
print(now.shortTime); // 14:30
print(now.formatted); // 2026-06-29 14:30
print(now.isWeekend); // false
Operators in Extensions
extension DurationExtension on Duration {
// Operator overload
Duration operator +(Duration other) {
return Duration(milliseconds: inMilliseconds + other.inMilliseconds);
}
Duration operator *(int multiplier) {
return Duration(milliseconds: inMilliseconds * multiplier);
}
// Comparison
bool operator <(Duration other) {
return inMilliseconds < other.inMilliseconds;
}
bool operator >(Duration other) {
return inMilliseconds > other.inMilliseconds;
}
}
// Usage
Duration d1 = Duration(seconds: 5);
Duration d2 = Duration(seconds: 3);
print(d1 + d2); // 8 seconds
print(d1 * 2); // 10 seconds
print(d1 > d2); // true
Generic Extensions
Extending Generic Types
// Extension on List<T>
extension ListExtension<T> on List<T> {
// Get second element safely
T? get second {
if (length >= 2) return this[1];
return null;
}
// Get last safely
T? get lastOrNull => isEmpty ? null : last;
// Safe access with default
T elementAtOrElse(int index, T defaultValue) {
if (index >= 0 && index < length) {
return this[index];
}
return defaultValue;
}
// Swap elements
void swap(int index1, int index2) {
if (index1 == index2) return;
final temp = this[index1];
this[index1] = this[index2];
this[index2] = temp;
}
}
// Usage
List<int> numbers = [1, 2, 3, 4, 5];
print(numbers.second); // 2
print(numbers.elementAtOrElse(10, 0)); // 0
numbers.swap(0, 4);
print(numbers); // [5, 2, 3, 4, 1]
Extending Map
extension MapExtension<K, V> on Map<K, V> {
// Safe get with default
V getOrElse(K key, V defaultValue) {
return this[key] ?? defaultValue;
}
// Get all keys with values
String toPrettyString() {
return entries.map((e) => '${e.key}: ${e.value}').join(', ');
}
// Filter map
Map<K, V> filter(bool Function(K key, V value) predicate) {
return Map.fromEntries(
entries.where((entry) => predicate(entry.key, entry.value))
);
}
}
// Usage
Map<String, int> scores = {'Alice': 95, 'Bob': 87};
print(scores.getOrElse('Charlie', 0)); // 0
print(scores.toPrettyString()); // Alice: 95, Bob: 87
print(scores.filter((k, v) => v > 90)); // {Alice: 95}
Extension on Nullable Types
Nullable Extensions
extension StringNullableExtension on String? {
// Safe methods for nullable strings
bool get isNullOrEmpty => this == null || this!.isEmpty;
bool get isNullOrBlank => this == null || this!.trim().isEmpty;
// Default value
String orDefault(String defaultValue) => this ?? defaultValue;
// Transform if not null
String? map(String Function(String) transform) {
if (this == null) return null;
return transform(this!);
}
}
// Usage
String? maybeName = null;
print(maybeName.isNullOrEmpty); // true
print(maybeName.orDefault('Guest')); // Guest
String? name = 'Alice';
print(name.map((s) => s.toUpperCase())); // ALICE
Nullable Numeric Extensions
extension IntNullableExtension on int? {
bool get isNull => this == null;
bool get isNotNull => this != null;
// Safe operations
int orZero() => this ?? 0;
int orDefault(int defaultValue) => this ?? defaultValue;
// Chain operations
int? add(int other) {
if (this == null) return null;
return this! + other;
}
int? multiply(int other) {
if (this == null) return null;
return this! * other;
}
}
// Usage
int? maybeNumber = 5;
print(maybeNumber.isNotNull); // true
print(maybeNumber.orZero()); // 5
print(maybeNumber.add(3)); // 8
int? nullNumber = null;
print(nullNumber.orZero()); // 0
print(nullNumber.add(3)); // null
Extension on Specific Types
Type-Specific Extensions
// Extension on List<int>
extension IntListExtension on List<int> {
// Stats
int get sum => reduce((a, b) => a + b);
int get product => reduce((a, b) => a * b);
// Stats with default
int? get average => isEmpty ? null : sum ~/ length;
// Filter
List<int> get even => where((n) => n % 2 == 0).toList();
List<int> get odd => where((n) => n % 2 != 0).toList();
}
// Extension on List<String>
extension StringListExtension on List<String> {
// Join with custom formatting
String get commaSeparated => join(', ');
String get newlineSeparated => join('\n');
// Transform
List<String> get upperCase => map((s) => s.toUpperCase()).toList();
List<String> get lowerCase => map((s) => s.toLowerCase()).toList();
// Filter
List<String> get nonEmpty => where((s) => s.isNotEmpty).toList();
}
// Usage
List<int> numbers = [1, 2, 3, 4, 5];
print(numbers.sum); // 15
print(numbers.even); // [2, 4]
List<String> names = ['Alice', 'Bob', 'Charlie'];
print(names.commaSeparated); // Alice, Bob, Charlie
print(names.upperCase); // [ALICE, BOB, CHARLIE]
Extension with Type Constraints
Constrained Extensions
// Extension only for Iterable of num
extension NumericIterableExtension<T extends num> on Iterable<T> {
// Works only with numeric types
T get sum {
if (isEmpty) return 0 as T;
return reduce((a, b) => (a + b) as T);
}
T? get average {
if (isEmpty) return null;
return sum / length as T;
}
}
// Extension only for Iterable of Comparable
extension ComparableIterableExtension<T extends Comparable<T>> on Iterable<T> {
T get max {
if (isEmpty) throw StateError('Cannot find max of empty iterable');
return reduce((a, b) => a.compareTo(b) > 0 ? a : b);
}
T get min {
if (isEmpty) throw StateError('Cannot find min of empty iterable');
return reduce((a, b) => a.compareTo(b) < 0 ? a : b);
}
Iterable<T> sorted([bool ascending = true]) {
final list = toList();
list.sort();
if (!ascending) list = list.reversed.toList();
return list;
}
}
// Usage
var numbers = [1, 2, 3, 4, 5];
print(numbers.sum); // 15
print(numbers.average); // 3
print(numbers.max); // 5
var names = ['Alice', 'Bob', 'Charlie'];
print(names.max); // 'Charlie' (alphabetically)
Extensions on Classes
Extending Your Own Classes
class Person {
final String name;
final int age;
Person(this.name, this.age);
}
// Extension on Person
extension PersonExtension on Person {
String get greeting => 'Hello, I am $name';
bool get isAdult => age >= 18;
bool get isSenior => age >= 65;
String get ageGroup {
if (age < 18) return 'Minor';
if (age < 65) return 'Adult';
return 'Senior';
}
Person copyWith({String? name, int? age}) {
return Person(
name ?? this.name,
age ?? this.age,
);
}
}
// Usage
var person = Person('Alice', 25);
print(person.greeting); // Hello, I am Alice
print(person.isAdult); // true
print(person.ageGroup); // Adult
var updated = person.copyWith(age: 30);
Extension Conflicts
Handling Conflicts
// Extension 1
extension StringExtension on String {
String reverse() => split('').reversed.join('');
int get lengthDouble => length * 2;
}
// Extension 2
extension StringExtension2 on String {
String reverse() => 'Reversed: ${split('').reversed.join('')}';
int get lengthTriple => length * 3;
}
// Usage with conflicts
String text = 'hello';
// Explicit extension usage
print(StringExtension(text).reverse()); // 'olleh'
print(StringExtension2(text).reverse()); // 'Reversed: olleh'
// Unambiguous names are fine
print(text.lengthDouble); // 10
print(text.lengthTriple); // 15
// Ambiguous method needs explicit extension name
Best Practices
Keep Extensions Focused
// Good: Focused extension
extension DateFormatter on DateTime {
String get yyyyMMdd => '${year}-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
String get ddMMyyyy => '${day.toString().padLeft(2, '0')}/${month.toString().padLeft(2, '0')}/$year';
}
// Bad: Too many unrelated methods
extension DateTimeUtils on DateTime {
String get yyyyMMdd => // ...
int get daysInMonth => // ...
bool get isWeekend => // ...
DateTime get nextMonday => // ...
String get timeZone => // ...
// Too many responsibilities!
}
Use Descriptive Names
// Good: Descriptive extension name
extension FileExtension on String {
bool get isImage => ['jpg', 'jpeg', 'png', 'gif', 'webp'].contains(extension);
String get extension => split('.').last.toLowerCase();
}
// Bad: Vague extension name
extension Str on String {
bool get isImg => // ...
}
Common Mistakes
Extending Too Broadly
Wrong:
// Too broad for Object
extension ObjectExtension on Object {
String toJson() => // ...
}
// Would add to ALL objects
Correct:
// Specific type
extension StringExtension on String {
String toJson() => '"$this"';
}
// Only for strings
Name Collisions
Wrong:
// Both extensions define the same method
extension A on String {
String format() => 'A: $this';
}
extension B on String {
String format() => 'B: $this';
}
// Usage: text.format() // Ambiguous!
Correct:
// Use different names or explicit extension
extension AFormatting on String {
String formatA() => 'A: $this';
}
extension BFormatting on String {
String formatB() => 'B: $this';
}
Summary
Extensions allow you to add functionality to existing types in a clean and organized way. They're great for adding utility methods, creating domain-specific APIs, and extending types you don't own.
Next Steps
Now that you understand extensions, continue to:
Did You Know?
- Extensions were introduced in Dart 2.6
- Extensions can be generic and constrained
- Extension names can be used to resolve conflicts
- Extensions can add operators to existing types
- You can extend nullable types
- Extensions work with Dart's type system
- Extensions are resolved at compile time