Edit: With null safety, this can’t be null anymore in an extension on a non-optional type.

We all know the this keyword in Dart (and many other languages). When used in a method, it refers to the instance of the object that the method is invoked on. Therefore, it can never be null. After all, you can’t invoke a method on null. Dart will throw a NoSuchMethodException. Right?

Extensions are a convenient method to add additional methods to a type when you can’t change that type, either because someone else defined it or because Dart simply doesn’t allow you to add methods to an enum. For example, I came across code like this recently:

Here, the extension defines a description getter on the State enum to get a user-readable description of each value. Interestingly, this code cause a linter warning:

info: This function has a return type of ‘String’, but doesn’t end with a return statement. (missing_return)

Surely, this must be a shortcoming of the linter: The switch-statement is exhaustive and this can’t be null, so there’s no way this code can execute and not reach one of the return statement. Or can it? Let’s try:

When this code runs, it prints:

“this is not null 😅”
“this is null 😨”

That’s right. No NoSuchMethodException when f is invoked on null and the execution enters the clause where this == null. How is this possible?

What this shows is that extension methods are really just syntactic sugar for free functions that have an object as a hidden, additional argument. So in this case, the f method in NullOrNotNull is something like NullOrNotNull_f(Foo this) { ... }. And obj.f() is converted, behind the scenes, to NullOrNotNull_f(obj).

Since the extension method f executed with null as its this, that also shows that Dart doesn’t rely on runtime types to determine which extension method to invoke. Dart uses the declared type to figure out which extension method to call. (The declared type is as Foo in this case, but it could have been the type of a variable or function argument: void someFunction(Foo foo) => foo.f().) To make this more clear, consider this example:

When this code runs, it will print “This is base” even though displaySomething is invoked on an instance of Derived. That is because description is called on value which type has been declared as Base on line 13.

Now, even if extension methods aren’t really methods, invoking methods on null isn’t that strange in Dart. You might have noticed that trying to invoke a method on null throws a NoSuchMethodException and not a “null pointer exception”. That’s because in Dart, null is not a null-pointer like in C++. It is an actual object of the type Null. It even implements some methods that can be invoked on it: null.toString() will execute just fine, for example.

In fact, the way that Dart finds the right extension method to run when something like obj.foo() is invoked, is by looking at all extensions that are in scope and have a foo method defined and choosing the one whose target type (the part after “on”) matches closest to the declared type of obj. So if a method name is unique, as was f in the example above, even null.f() will work. If there is only one match, per definition it is the closest. Therefore, the as Foo part in the example was redundant.

So, what have we learned? When it appears in an extension method this can be null. In fact, all uniquely named extension methods can be directly invoked on null like null.uniqueExtensionMethod(). And finally, to figure out which extension method to run, Dart will look at the declared type of the variable and ignore the run-time type.

Physicist. Software developer.