Extension methods
An extension adds methods, getters, setters and operators to an existing type without touching its source. It works on Dart's own String, int and List, and equally on classes from someone else's package.
Declaring one:
extension StringX on String { bool get isBlank => trim().isEmpty; }- You may leave it unnamed (
extension on String { ... }), but give it a name — on a conflict,show/hideand explicit invocation only work with a name. - It can be generic:
extension ListX<T> on List<T> { T? get firstOrNull => ... } - It can be declared on a nullable type:
extension on String? { ... }— therethismay benull.
Inside an extension, this is the receiver and usually needs no writing: trim() already means this.trim().
Static resolution — the favourite interview trap. Extension members are picked at compile time, based on the variable's static type. That is a fundamental difference from ordinary methods, which are found at runtime from the object's real type (dynamic dispatch).
The consequences:
Dog dog = Dog(); dog.speak();→DogX.speak()runs.Animal a = dog; a.speak();→ the same object, yetAnimalX.speak()runs, because the static type isAnimal.dynamic d = dog; d.speak();→ a runtime error: extensions are never looked up ondynamic.- An extension method cannot be overridden — writing an extension with the same name on a subclass is not an override, just a different extension.
Remember one more rule: the type's own member always wins. String has length, so a length in your extension is never called. An extension only kicks in when the type has no such name — which is why your code does not break when the Dart team adds a method to String, though behaviour may change.
| Question | Extension method | Ordinary instance method |
|---|---|---|
| When is it resolved? | At compile time, from the static type | At runtime, from the real type |
| Can it be overridden? | No | Yes |
| Does it work on `dynamic`? | No — a runtime `NoSuchMethodError` | Yes |
| Can it reach private fields? | Only within the same library | Yes |
| Does it depend on imports? | Yes — the extension must be in scope | No |
| On a clash with the type's own member | It loses — the type's own member wins | Not applicable |
Conflicts. If two imports bring in extension members with the same name for the same type, the compiler cannot choose and reports an error. There are three fixes:
import 'b.dart' hide Trimmer;— hide one of them.import 'b.dart' show Squeezer;— bring in only what you need.- Explicit application:
Trimmer(' x ').squeeze()— invoke via the extension's name. This is the most precise route, independent of imports, and it is also how you bypass static-type resolution:AnimalX(dog).speak().
When does an extension help, and when does it hurt?
It helps when it genuinely changes how the code reads (19999.azn, '2026-01-01'.toDate(), list.firstOrNull), or when the method you want belongs to a type you do not own.
It hurts when it hides a cost (a network call or heavy computation behind a getter), when it piles names onto very general types (Object, int) — every int now answers to that name and autocomplete fills with noise — and when it lets the same idea be reimplemented as two competing extensions in one codebase.
Interview tip. The question nearly always arrives in the same shape: "what does `Animal a = dog; a.speak();` print?". The right answer structure: (1) lead with "extensions are resolved at compile time, from the static type"; (2) therefore AnimalX.speak() runs, not DogX, even though the object is a Dog; (3) add the contrast: had it been an ordinary method, dynamic dispatch would have run Dog's version.
The most common mistake is describing an extension as "adding a method to a class". Nothing is added to the class at all: the compiler simply rewrites a.speak() into the static call AnimalX.speak(a). That single sentence explains the whole behaviour — why it fails on dynamic, why it cannot be overridden, and why it depends on imports.
📚 Sources and documentation
- Extension methodsofficialdart.dev