| |

Dart 29 🎯 Extension Methods (extension on) and Static Extensions

Dart does not allow you to add methods to a class you do not own. The String class is in the SDK, and you cannot edit it. A third-party library’s DateTime class is sealed to you. But you can add methods to them anyway. Extensions are a Dart feature that lets you add members to any type—from the SDK, from a package, or from your own code—without modifying the type or creating a subclass. The methods appear to be part of the type, and the call site looks like a normal method call.

This chapter covers extension methods in full. You will learn the extension declaration, the on clause that specifies the type being extended, the members an extension can add, and the rules that govern when an extension is visible. You will also see static extensions, the extension type feature added in Dart 3.3, and the patterns that make extensions maintainable.

Key point: An extension is a named block of members that can be called on values of a specific type. The on clause specifies the type. The members are not actually added to the type—they are resolved by the compiler at the call site. An extension must be in scope for its members to be available. Two extensions that define the same member on the same type produce a conflict, which the compiler resolves by local scope or rejects as ambiguous.


Why extensions exist

The ownership problem. You cannot add methods to a class you do not own. If you want a isEmail getter on String, you cannot edit the String class. You could write a top-level function isEmail(String s), but then the call site is isEmail(email) instead of email.isEmail. The extension makes the call site read like a method while keeping the implementation outside the class.

The utility problem. Utility functions that operate on a specific type are natural candidates for extensions. A String extension with capitalize, truncate, and isNumeric groups the utilities under the type they operate on. The call site reads naturally: text.capitalize(), text.truncate(20), text.isNumeric.

The third-party problem. A third-party library may provide a class without the convenience methods you need. You cannot fork the library just to add a method. An extension lets you add the method in your own code, and the call site is indistinguishable from a method that the library provides.

The generic problem. Extensions can be declared on generic types. An extension on List<T> adds methods to every list, regardless of the element type. An extension on List<int> adds methods only to lists of integers. This allows type-specific utilities that are still ergonomic.

The conflict problem. Two libraries may define an extension with the same member name on the same type. The compiler resolves the conflict based on scope: an extension in the current file takes precedence over one in an imported file. If both are in scope with equal precedence, the call is ambiguous and the compiler reports an error. This is why extension names should be unique and extension methods should not be too generic.

The extension type problem. Dart 3.3 introduced extension type, which is a different feature with a similar name. An extension type is a wrapper type that provides a new interface for an existing type without the runtime cost of a wrapper class. It is used for zero-cost type safety and for adding members to an existing type in a way that is visible in the type system. This chapter covers both.


a. Declaring and using extensions

An extension is declared with the extension keyword, an optional name, and an on clause that specifies the type being extended.

extension StringHelpers on String {
  String get capitalized =>
      isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';

  bool get isEmail => contains('@') && contains('.');

  String truncate(int maxLength) =>
      length <= maxLength ? this : '${substring(0, maxLength)}...';
}

The extension is named StringHelpers. The on String clause specifies that the members are available on values of type String. The extension adds a getter capitalized, a getter isEmail, and a method truncate.

The members are called as if they were methods on the type:

void main() {
  print('hello'.capitalized);       // 'Hello'
  print('a@b.com'.isEmail);         // true
  print('a very long string'.truncate(5)); // 'a ver...'
}

Inside the extension, this refers to the value the member was called on. The capitalized getter uses this implicitly and substring, which is a method on String.

An extension can be unnamed. The name is optional, but naming is recommended because it helps with conflict resolution and documentation.

extension on int {
  bool get isEven => this % 2 == 0;
}

print(4.isEven); // true

The unnamed extension works the same way. It cannot be referenced by name, so it is harder to disambiguate if a conflict occurs.

An extension can add operators. Operator declarations in an extension follow the same syntax as in a class.

extension StringMultiply on String {
  String operator *(int times) => List.filled(times, this).join();
}

print('ab' * 3); // 'ababab'

The * operator is added to String. The call site reads as if the operator were defined on the type.

An extension can add getters, setters, methods, and operators. It cannot add fields. A field would require storage, and an extension does not have storage—it only adds members that are resolved at the call site. A getter that computes a value is fine; a field is not.

extension Counter on int {
  // int get double => this * 2;  // fine
  // int value = 0;                // error: cannot add a field
}

The on clause can be a generic type:

extension ListHelpers<T> on List<T> {
  T? get firstOrNull => isEmpty ? null : first;
  T? get lastOrNull => isEmpty ? null : last;
}

print([1, 2, 3].firstOrNull); // 1
print(<int>[].firstOrNull);   // null

The extension applies to any List<T>. The type parameter T is inferred at the call site.

The on clause can be a specific instantiation:

extension IntListHelpers on List<int> {
  int get sum => fold(0, (a, b) => a + b);
}

print([1, 2, 3].sum); // 6
// print(['a', 'b'].sum); // error: no sum on List<String>

The extension applies only to List<int>. A List<String> does not have the sum getter.


b. Scope, conflicts, and resolution

An extension is available only when it is in scope. It is in scope if it is declared in the current library, imported, or exported by an import.

// helpers.dart
extension StringHelpers on String {
  String get capitalized => ...;
}

// main.dart
import 'helpers.dart';

void main() {
  print('hello'.capitalized); // available because helpers.dart is imported
}

The extension is in scope in main.dart because helpers.dart is imported. If the import is removed, the capitalized getter is no longer available.

Extensions can be imported with a prefix or hidden:

import 'helpers.dart' show StringHelpers;
import 'other.dart' hide OtherExtension;

The show and hide clauses apply to extensions the same way they apply to other declarations. An extension that is hidden or not shown is not in scope.

When two extensions define the same member on the same type, the compiler resolves the conflict by scope. An extension in the current library takes precedence over one in an imported library. If both are imported with equal precedence, the call is ambiguous.

// a.dart
extension A on String {
  String get greeting => 'A';
}

// b.dart
extension B on String {
  String get greeting => 'B';
}

// main.dart
import 'a.dart';
import 'b.dart';

void main() {
  print('x'.greeting); // error: ambiguous
}

The error is reported at the call site. The fix is to import one of the extensions with a prefix, hide one, or use the extension name explicitly:

import 'a.dart';
import 'b.dart' hide B;

print('x'.greeting); // 'A'

Or the explicit form:

print(A('x').greeting); // 'A'

The explicit form uses the extension name as a function-like call. The first argument is the value. This disambiguates without changing the imports.

An extension can be applied explicitly even when it is not in scope, as long as it is imported:

import 'helpers.dart' as helpers;

print(helpers.StringHelpers('hello').capitalized);

This is verbose but unambiguous.


c. Static extensions and extension types

An extension can define static members. Static members are accessed on the extension name, not on a value.

extension StringValidation on String {
  static bool isValidEmail(String value) {
    return value.contains('@') && value.contains('.');
  }
}

print(StringValidation.isValidEmail('a@b.com')); // true

The static method is called on the extension name. It does not use this because there is no value. This is a way to namespace utility functions under a related type.

Dart 3.3 introduced extension type, which is a different feature. An extension type is a wrapper that provides a new interface for an existing type, with no runtime cost.

extension type UserId(int value) {
  bool get isValid => value > 0;
}

The UserId is an extension type that wraps an int. It has the isValid getter. At runtime, a UserId is an int—there is no wrapper object. The type is erased.

void main() {
  var id = UserId(42);
  print(id.value);   // 42
  print(id.isValid); // true
}

The value is the representation, accessible as a property. The extension type adds members to the representation type without changing its runtime representation.

An extension type can implement interfaces:

extension type UserId(int value) implements Comparable<UserId> {
  bool get isValid => value > 0;

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

The UserId is a distinct type from int at compile time, but it is represented as an int at runtime. This is useful for type safety: a function that takes a UserId cannot be passed a raw int without a conversion.

void printUserId(UserId id) {
  print(id.value);
}

printUserId(UserId(42)); // fine
// printUserId(42);       // error: int is not UserId

The conversion is explicit: UserId(42) wraps the integer, and id.value unwraps it. The type system prevents accidentally passing an arbitrary integer where a UserId is expected.

An extension type can have a named constructor:

extension type EmailAddress(String value) {
  EmailAddress.parse(String input) : value = input.trim().toLowerCase();

  bool get isValid => value.contains('@');
}

The EmailAddress.parse constructor trims and lowercases the input. The isValid getter checks the format. The value is stored as the representation type, a String.

An extension type can be declared as an extension type or, for the version that includes static members and a separate interface, extension type with implements. The two are slightly different in their rules, but the common case is the simple wrapper.

A comparison with regular extensions: a regular extension adds methods to a type without changing the type. An extension type creates a new type that is distinct from the representation type at compile time. Use a regular extension when you want to add convenience methods to a type you do not own. Use an extension type when you want a distinct type for type safety while keeping the same runtime representation.


Complete Example Session

// ============================================
// PART 1: BASIC EXTENSION
// ============================================
extension StringHelpers on String {
  String get capitalized =>
      isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
}

void main() {
  print('hello'.capitalized); // 'Hello'
}
// ============================================
// PART 2: EXTENSION WITH METHOD
// ============================================
extension StringTruncate on String {
  String truncate(int maxLength) =>
      length <= maxLength ? this : '${substring(0, maxLength)}...';
}

print('a very long string'.truncate(5)); // 'a ver...'
// ============================================
// PART 3: EXTENSION WITH GETTER
// ============================================
extension StringValidation on String {
  bool get isEmail => contains('@') && contains('.');
}

print('a@b.com'.isEmail); // true
// ============================================
// PART 4: EXTENSION WITH OPERATOR
// ============================================
extension StringMultiply on String {
  String operator *(int times) => List.filled(times, this).join();
}

print('ab' * 3); // 'ababab'
// ============================================
// PART 5: GENERIC EXTENSION
// ============================================
extension ListHelpers<T> on List<T> {
  T? get firstOrNull => isEmpty ? null : first;
  T? get lastOrNull => isEmpty ? null : last;
}

print([1, 2, 3].firstOrNull); // 1
print(<int>[].firstOrNull);   // null
// ============================================
// PART 6: SPECIFIC INSTANTIATION
// ============================================
extension IntListHelpers on List<int> {
  int get sum => fold(0, (a, b) => a + b);
}

print([1, 2, 3].sum); // 6
// ============================================
// PART 7: EXTENSION CONFLICT
// ============================================
// a.dart
extension A on String {
  String get greeting => 'A';
}

// b.dart
extension B on String {
  String get greeting => 'B';
}

// main.dart
// import 'a.dart';
// import 'b.dart';
// print('x'.greeting); // ambiguous
// ============================================
// PART 8: EXPLICIT EXTENSION CALL
// ============================================
print(A('x').greeting); // 'A'
print(B('x').greeting); // 'B'
// ============================================
// PART 9: STATIC EXTENSION MEMBERS
// ============================================
extension StringValidation on String {
  static bool isValidEmail(String value) {
    return value.contains('@') && value.contains('.');
  }
}

print(StringValidation.isValidEmail('a@b.com')); // true
// ============================================
// PART 10: EXTENSION TYPE
// ============================================
extension type UserId(int value) {
  bool get isValid => value > 0;
}

void printUserId(UserId id) {
  print(id.value);
}

printUserId(UserId(42));

The ten parts covered basic extensions, methods, getters, operators, generic extensions, specific instantiations, conflicts, explicit calls, static members, and extension types.


Quick Reference

Extension Declaration

PartSyntax
Keywordextension
NameOptional
Typeon Type
MembersGetters, setters, methods, operators
Static membersstatic
FieldsNot allowed

Extension Usage

OperationSyntax
Call membervalue.member
Explicit callExtensionName(value).member
Static callExtensionName.staticMember
Conflict resolutionshow, hide, prefix, explicit

Extension Type

FeatureSyntax
Declarationextension type Name(Type value)
Representationvalue property
MembersSame as class
ConstructorsNamed constructors allowed
Interfacesimplements
RuntimeErased to representation type

Extension vs Extension Type

AspectExtensionExtension Type
Adds members toExisting typeNew type
Runtime costNoneNone
Type distinctionNoneDistinct type
FieldsNot allowedAllowed (as representation)
InterfacesNot applicableCan implement
Use caseConvenience methodsType safety

Best Practices

✅ Do This:

// Name your extensions
extension StringHelpers on String { }                          // ✅

// Group related members in one extension
extension StringValidation on String {
  bool get isEmail => ...;
  bool get isUrl => ...;
}                                                              // ✅

// Use generic extensions for reusable helpers
extension ListHelpers<T> on List<T> { }                        // ✅

// Use extension types for type safety
extension type UserId(int value) { }                           // ✅

// Disambiguate conflicts explicitly
print(StringHelpers('x').capitalized);                         // ✅

// Use show/hide to control scope
import 'helpers.dart' show StringHelpers;                      // ✅

❌ Don’t Do This:

// Don't add fields to an extension
extension Bad on String {
  int value = 0;  // error                                       // ❌
}

// Don't define the same member in two extensions
extension A on String { String get x => 'A'; }
extension B on String { String get x => 'B'; }                  // ❌ ambiguous

// Don't use generic names for extension members
extension on String {
  String get value => this;  // too generic                     // ⚠️
}

// Don't extend types you do not own with breaking changes
extension on Object {
  void log() => print(this);  // applies to everything          // ⚠️
}

// Don't use extension types when a regular extension fits
extension type Name(String value) { }                          // ⚠️

// Don't ignore conflicts silently
// Resolve with show/hide or explicit call                       // ❌

Common Pitfalls

PitfallWhy It HappensFix
Extension not availableNot in scopeImport the file
Ambiguous callTwo extensions in scopeUse show, hide, or explicit call
Extension has no effectType mismatchCheck the on clause
Fields not allowedExtensions have no storageUse getters
Extension type confused with extensionSimilar namesCheck the use case
Static member called on valueStatic is on the extensionUse the extension name
Extension lost after refactorRemoved importKeep imports explicit

Real-World Examples

1. String Capitalization

extension StringCapitalize on String {
  String get capitalized =>
      isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
}

2. String Truncation

extension StringTruncate on String {
  String truncate(int max) =>
      length <= max ? this : '${substring(0, max)}...';
}

3. List Helpers

extension ListHelpers<T> on List<T> {
  T? get firstOrNull => isEmpty ? null : first;
  T? get lastOrNull => isEmpty ? null : last;
}

4. Number Formatting

extension NumberFormatting on num {
  String get currency => '\$${toStringAsFixed(2)}';
}

5. DateTime Helpers

extension DateHelpers on DateTime {
  bool get isToday {
    final now = DateTime.now();
    return year == now.year && month == now.month && day == now.day;
  }
}

6. Iterable Helpers

extension IterableHelpers<T> on Iterable<T> {
  Iterable<T> whereNotNull() => where((e) => e != null).cast<T>();
}

7. Map Helpers

extension MapHelpers<K, V> on Map<K, V> {
  V getOrElse(K key, V defaultValue) => this[key] ?? defaultValue;
}

8. Extension Type for IDs

extension type OrderId(String value) {
  bool get isValid => value.isNotEmpty;
}

9. Extension Type for Email

extension type EmailAddress(String value) {
  EmailAddress.parse(String input) : value = input.trim().toLowerCase();
  bool get isValid => value.contains('@');
}

10. Static Extension

extension StringValidation on String {
  static bool isValidEmail(String value) =>
      value.contains('@') && value.contains('.');
}

Visual

Extension Resolution

┌─────────────────────────────────────────────────────────────┐
│  extension StringHelpers on String {                        │
│    String get capitalized => ...;                           │
│  }                                                          │
│                                                             │
│  'hello'.capitalized                                        │
│    │                                                        │
│    ▼                                                        │
│  Compiler: is `capitalized` a member of String?             │
│    ├── NO ──▶ Search extensions in scope                    │
│    │          │                                              │
│    │          ├── StringHelpers found ──▶ use it            │
│    │          │                                              │
│    │          └── Not found ──▶ error                       │
│    │                                                        │
│    └── YES ──▶ use the class member                         │
│                                                             │
│  Extensions are resolved at compile time.                   │
│  They are not actual members of the type.                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Conflict Resolution

┌─────────────────────────────────────────────────────────────┐
│  extension A on String { String get x => 'A'; }             │
│  extension B on String { String get x => 'B'; }             │
│                                                             │
│  Both imported, both in scope.                              │
│                                                             │
│  'text'.x                                                   │
│    │                                                        │
│    ▼                                                        │
│  Ambiguous! Compiler error.                                 │
│                                                             │
│  Solutions:                                                 │
│  1. import 'b.dart' hide B;                                 │
│  2. A('text').x  → 'A'                                      │
│  3. B('text').x  → 'B'                                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Extension vs Extension Type

┌─────────────────────────────────────────────────────────────┐
│  EXTENSION                                                  │
│                                                             │
│  extension StringHelpers on String {                        │
│    String get capitalized => ...;                           │
│  }                                                          │
│                                                             │
│  Adds a method to String.                                   │
│  String is still String.                                    │
│  No new type is created.                                    │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  EXTENSION TYPE                                             │
│                                                             │
│  extension type UserId(int value) {                         │
│    bool get isValid => value > 0;                           │
│  }                                                          │
│                                                             │
│  Creates a new type UserId.                                 │
│  At compile time, UserId is distinct from int.              │
│  At runtime, UserId is an int.                              │
│  Type safety without runtime cost.                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Extension Type Erasure

┌─────────────────────────────────────────────────────────────┐
│  COMPILE TIME                                               │
│                                                             │
│  extension type UserId(int value) { }                       │
│                                                             │
│  UserId is a distinct type.                                 │
│  void f(UserId id) { }                                      │
│  f(42);         // error: int is not UserId                 │
│  f(UserId(42)); // OK                                       │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  RUNTIME                                                    │
│                                                             │
│  UserId(42) is represented as the int 42.                   │
│  No wrapper object.                                         │
│  No memory overhead.                                        │
│                                                             │
│  The type exists only at compile time.                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
Declarationextension Name on Type { }
MembersGetters, setters, methods, operators
FieldsNot allowed
ScopeMust be imported
ConflictResolved by scope or explicit call
Explicit callExtensionName(value).member
Static membersExtensionName.staticMember
Generic extensionextension Name<T> on List<T>
Specific extensionextension Name on List<int>
Extension typeextension type Name(Type value)
Extension type runtimeErased to representation
Extension type interfacesimplements

Key takeaways:

  • An extension adds members to a type without modifying it. The members are resolved by the compiler at the call site. The type itself is unchanged, and no subclass is created.
  • The on clause specifies the type being extended. It can be a concrete type, a generic type, or a specific instantiation. The members are available on any value of that type when the extension is in scope.
  • Extensions cannot have fields. They have no storage. Members that appear to be fields are getters that compute a value. This is a deliberate limitation, not a restriction to work around.
  • Extensions are resolved by scope. An extension must be imported to be available. Two extensions with the same member on the same type produce an ambiguity, which is resolved by show, hide, a prefix, or an explicit call.
  • Static members are called on the extension name. They do not use this and are not available on values. They are a way to namespace utility functions under a related type.
  • Extension types are a different feature. An extension type creates a new type that is distinct from its representation at compile time and erased to it at runtime. It is used for type safety without runtime cost.
  • Use regular extensions for convenience methods on types you do not own. Use extension types when you need a distinct type for type safety while keeping the same runtime representation.

Remember: Extensions are a way to add members to types you cannot modify. They are resolved at compile time, they have no storage, and they are visible only when in scope. Use them for convenience methods, validation helpers, and type-specific utilities. Name them clearly, group related members, and resolve conflicts explicitly. When you need a distinct type rather than additional members, use an extension type. The two features share a name but solve different problems. Extensions add behavior to a type; extension types create a new type. Knowing which one to reach for is the design decision that matters.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!