Dart extension method fundamentals
Read about the design considerations behind extension methods, a new feature landing in a future Dart SDK release
Dart Extension Methods Fundamentals
#
In a future release, the Dart language is adding a new
feature, extension methods, which allows you to
(pretend to) add new members to existing types. An extension
method can be invoked just like a normal method,
o.extensionMethod(42), even though it really is
just a static function.
Why are we adding extension methods? What are they good for?
How do you use them? And why do I call them “extension
methods” when you can add other members too? (The
last one is easy: I personally think of them as extension
members, but “extension methods” was the working
title, and it’s what the similar feature is called in other
languages, so in good Dart tradition, we went for the
familiar and unsurprising name. I won’t be needing extension
getters, setters or operators here, but you can totally add
a % operator to String if you want
to, no matter what we call the feature.)
Since I’m one of the people who designed the feature, I’ll opportunistically answer all these questions before anyone else has a chance. (And, because I released this article before we finished the feature, I’ve even gotten to edit out the things that are no longer true!)
But first, a detour!
What I Would Have Done Before Extension Methods
#
Assume, purely hypothetically, that I think the
catchError
function on
Future
is awful and should be replaced by something newer, shinier
and better. Say, because it takes aFunction
as argument instead of a proper function type, which it does
for perfectly reasonable historical
reasons, and that means you won’t get any static type
checking. That is bad, and the method should feel bad.
Obviously I can’t remove the function, that would break, like, every serious Dart program ever.
Then I’d want to at least add a new method to
Future<T> so that users can use
that one instead, say one like:
abstract class Future<T> {
...
/// Catches any [error] of type [E].
Future<T> onError<E>(FutureOr<T> handleError(E error, StackTrace stack)) =>
this.catchError(...something clever...);
}
which you can call as:
Future<int> eventualInteger = ...;
eventualInteger.onError((FormatException e, s) => ...).then(...);
Sadly, I can’t just add that to the
Future class. If I do that, I also add it to
the Future
interface, and any other class implementing that interface
will then be incomplete and will no longer compile. At some
point we counted 76 classes implementing
Future. That was a while ago, and we’ve stopped
counting. We still can't break everybody, so that option is
also not on the table.
Well, then I’ll use a static helper function:
Future<T> onFutureError<T, E>(Future<T> source,
FutureOr<T> handleError(E error, StackTrace stack)) =>
source.catchError(...something clever...);
which you can call as:
Future<int> eventualInteger = ...;
onFutureError(eventualInteger,
(FormatException e, s) => ...).then(...);
Almost as sadly, that simply doesn’t read well. We
like using .-based method chaining
because it allows us to read from left to right: “Do this,
then do that, then do something more”. Using the static
helper function forces us to read it as: “Do that to the
following: Do this. After that, do something more” … say
what? It just doesn't have the same flow, the same
schwung. It's darn nigh unreadable in practice.
OK then, I’m undeterred in my quest, so instead of improving
the Future class, I'll introduce a new and
improved interface and give users a way to wrap the old
interface:
class MyFuture<T> {
Future<T> _wrappee;
MyFuture(Future<T> future) : _wrappee = future;
Future<T> onError<E>(
FutureOr<T> handleError(E error, StackTrace stack)) =>
_wrappee.catchError(...something clever...);
}
which you can use as:
Future<int> eventualInteger = ...;
MyFuture(eventualInteger).onError(
(FormatException e, s) => ...).then(...);
I’d probably even let MyFuture implement
Future and forward all the
Future members to the
_wrappee future, and then also let all the
methods return a MyFuture
wrapper again, so I can keep going.
class MyFuture<T> {
Future<T> _wrappee;
MyFuture(Future<T> future) : _wrappee = future;
MyFuture<R> then<R>(...) => MyFuture(_wrappee.then<R>(...));
/// Forward other `Future` methods too.
MyFuture<T> onError<E>(
FutureOr<T> handleError(E error, StackTrace stack)) =>
MyFuture(_wrappee.catchError(...something clever...));
}
Nice and smooth, if I do say so myself!
This was pretty much as good as it got, back before extension methods … which meant manually adding the wrapper and taking the performance hit from the extra wrapper objects and intermediate forwarding functions.
What I Will Do With Extension Methods
#Once we’re out of the dark no-extension ages, I can use extension methods to get what I really, really want. I’ll write:
extension MyFuture<T> on Future<T> {
Future<T> onError<E>(
FutureOr<T> handleError(E error, StackTrace stack)) =>
this.catchError(...something clever...);
}
and then you can call it as:
Future<int> eventualInteger = ...;
eventualInteger.onError((FormatException e, s) => ...).then(...);
and that is all. Mission accomplished in five lines!
“But how does it work?”, you might ask. It works very well, thank you.
In truth,
it behaves almost exactly the same way as the wrapper
class, even though it really is just a static helper function.
You can even write
MyFuture(eventualInteger).onError(...)
explicitly, as if the extension was a wrapper class. It
isn't, but it looks and acts almost as if it was.
And you can omit the explicit wrapping and have it
implicitly applied when the types are right.
It’s (not) a Wrapper Class
#
The design of the extension declaration is
deliberately made so that it looks like a
class or mixin declaration, and it
acts just as if it was a wrapper class with a hidden
_wrappee. You can even have static members in
the declaration, and they work just like static members on a
class or mixin declaration.
There is one improvement over the wrapper class:
You can write this inside instance members to
refer to the _wrappee instead of the wrapper
object.
Changing the meaning of this was not just an
improvement. These are static extension methods,
and as I said earlier, they are really just a more
convenient way to call static functions. That means that
there is no wrapper object. It never existed, we
just pretended that it did, but that means that we can't let
this
refer to the non-existing object.
We also can't allow you to use
MyFuture(eventualInteger) as a value, so if you
try to do
var myFuture = MyFuture(eventualInteger), we
won't allow it. The only way to use
MyFuture(eventualInteger)
is as the target of an extension member invocation.
MyFuture(eventualInteger).onError(...); // GOOD: Use to call method.
var x = MyFuture(eventualInteger); // BAD: Use as stand-alone value.
It’s the same as how you can use super to call
methods, but not for its value. Or just like a library
prefix. All you can do is access a member; you cannot treat
it as a value because it has no value, and there is no value
for it to have.
Because there is no object, you can’t declare
instance fields in an
extension declaration. You can declare
getters and setters though, and perhaps even back them by an
Expando. An extension also cannot declare any constructors since
nothing is being constructed; it just pretends to have a
constructor taking the wrappee object.
It Does (not) Extend the Type
#
If you had to write the MyFuture(...) wrapping
every time you used an extension member, then it wouldn't be
much of an improvement. We could probably just write the
wrapper classes directly and spend some compiler engineer
hours ensuring that we optimize away the intermediate
object.
I said above that you can write
eventualInteger.onError(...). This works
because we implicitly wrap expressions based on
their static type and the name of the member they call. We
automatically wrap expr.method() as
Ext(expr).method() when all of the following
are true:
-
The static type of
exprdoes not have a member with (base-)namemethod(the interface always wins). -
The extension
Extis imported or declared in the current library scope (the extension is accessible). -
The extension declares a member with base-name
method, and the static type ofexpris a subtype of theontype of theExtdeclaration (the extension is applicable).
If there is more than one accessible and applicable
extension for a member invocation, there are rules about
which one will win the conflict. In some cases, there’s no
way to pick a winner, and then it’s just a compile-time
error. These rules depend only on the extension
declaration’s
on type, not on member declaration. (Dart does
not have “overloading” — multiple methods with the same name
and different signatures, which you choose between based on
the argument structure or types — and extension methods do
not provide a back-door to get overloading.)
It Is All Static
#I said “static extension methods” above, and I did so for a reason!
Dart is statically typed. The compiler knows the type of
every expression at compile-time, so if you write
target.member(42), and member is
an extension member, then the compiler needs to figure out
which extension to implicitly wrap
target with, in order to find the type of the
entire member invocation.
If implicit extension wrapping has to happen between finding the type of the target expression and finding the type of the member invocation, it seems obvious that “extension inference” has to happen during the increasingly more inaccurately named “type inference” phase. That’s the phase which is mostly known for filling in missing generics.
I did write
eventualInteger.onError((FormatException e, s)
{...}), even though both the MyFuture extension and
the onError method are generic. While doing
type inference, the Dart compiler both selects the extension
and infers missing type arguments. Here it first decides to
use the MyFuture extension, then inserts the
implicit wrapper, and finally performs type inference for
the extension application
MyFuture(eventualInteger).onError((FormatException e, s)
{...})
in exactly the same way
as it would for a corresponding wrapper class:
class MyFuture<T> {
Future<T> _wrappee;
MyFuture(Future<T> future) : _wrappee = future;
MyFuture<T> onError<E>(
FutureOr<T> handleError(E error, StackTrace stack)) =>
_wrappee.catchError(...something clever...);
}
In this case, the type inference would infer the following extension application and complete types for the invocation:
MyFuture<int>(eventualInteger).onError<FormatException>(
(FormatException e, StackTrace s) {...});
This means that
the type arguments to the extension are based on the
static type of the wrapped expression. If you have a
Future<num> fut =
Future<int>.value(42);
then fut.onError(...) will bind the
T type parameter of MyFuture to
num at compile-time, not to int.
It's all static, just as for any other inferred type
arguments.
That also means that you will never be able to call an
extension member on a target typed as dynamic.
Conflict Resolution
#
As stated above, there are rules about which extension wins
when there is more than one in scope which applies.
Basically, the winner is the extension with an
on type closest to the actual type of the
expression you are calling a member on, with some caveats
and tie-breakers. It usually “just works” for extensions
which are written together. Instead of going into those
details, I'll tell you what to do when it
doesn't just work.
You might have a problem when two different authors have
written conflicting extensions for the same type and member
name. Say the extensions
Ext1 and Ext2 both define a
bubbleSort method that applies to your
List object, and either there is no clear
winner of the conflict, or the one which wins is not the one
you actually want to call (say Ext2 wins and
you want to call Ext1.bubbleSort). Then you
have to do something.
The easiest solution is to use an explicit extension
application: Ext1(list).bubbleSort(). This
avoids the automatic resolution and just picks the one that
you want. If you only have a few conflicts, then that's both
easy and readable.
However, if you have three hundred conflicts in the same file, then you might want to avoid the extra typing. It’s hard to change whether an extension is applicable to an invocation, but you can change whether it’s accessible.
You do that by hiding the conflicting extension (or
extensions, if you are really unlucky) where you
import it: import "ext2lib.dart" hide Ext2;.
Doing so will prevent the Ext2 extension from
being imported into the current library scope, which makes
it not accessible. Obviously, so will not importing
ext2lib.dart at all, but unless the extension
is the only thing you use from that library, that’s not
practical.
(Edited Dec. 11th) Here I used to say that you could import one of the conflicting extensions with a prefix and that then it would not be available for implicit use. Turns out that some people declare extension methods in the same library as the class they extend, and it is really annoying if that library doesn’t work when imported with a prefix. So we fixed that. Extensions imported with a prefix do work implicitly as well. If you really need to use two conflicting extension in the same library, you will have to use explicit extension application everywhere there is a conflict. We may consider adding a different way to disable implicit extensions in the future, at least if conflicting extensions turn out to be a recurring issue.
Summary
#Dart will get extension methods in an upcoming release — a pretty way to call static functions.
You can define extension members for instance methods, operators, setters and getters, but not fields.
You can invoke extension methods either explicitly or — when there’s no conflict with an interface member or another extension — implicitly:
Ext1(list).bubbleSort() // Explicit, like it's a wrapper class.
list.bubbleSort() // Implicitly, like it extends the type.
Implicit invocations work the same as explicit invocations, but they first infer which extension is being applied. If extension inference fails due to conflicting extensions, then you can do any one of the following:
Apply the extension explicitly.
-
Don’t import the conflicting extension at all (remove the import or hide the extension).
(Edited Dec 11th): And that is it (for now).
Extensions are static. Everything about them is decided based on static types.
Enjoy responsibly!
More from Dart
Dart in Google Summer of Code 2026
We’re excited to announce that the Dart project will mentor contributors for the seventh time in Google Summer of Code 2026!
Google Summer of Code 2025 Results
The Google Summer of Code (GSoC) program focuses on engaging new developers worldwide with open source software development. Google…