Dart Extension Methods Tutorial (incl. Generic Extensions, Properties & Operators)

Dart 2.6 is just around the corner. In fact, it may as well be out already as you're reading this. Dart has lived through a revival linked with the popularity of Flutter and people responsible for bringing new features into this language can't seem to stop working! There is one big feature which we were asking for all along and now it's here - extension members.

Set up Dart 2.6

As you're reading this, Dart 2.6 may already be officially released. In that case, you're good! Otherwise, install the Dart SDK from the UNSTABLE channel following these instructions. Then, to prevent any warnings about using features which aren't guaranteed to exist, update the pubspec.yaml file.

pubspec.yaml

environment:
  sdk: '>=2.6.0 <3.0.0'

Why extensions? 🤔

Let's put it this way - every single language which supports extensions, benefits from them immensely. They allow you to get rid of utility classes littered with a bunch of static methods and turn them into a beautiful "work of art methods" instead. Imagine this legacy code:

main.dart

class StringUtil {
  static bool isValidEmail(String str) {
    final emailRegExp = RegExp(r"^[a-zA-Z0-9.][email protected][a-zA-Z0-9]+\.[a-zA-Z]+");
    return emailRegExp.hasMatch(str);
  }
}

// Usage
main() {
  StringUtil.isValidEmail('someString');
}

Using the StringUtil class is redundant. Also, we're all spoiled by OOP to call methods on an instance directly and now we're passing the String instance into a static method. What if we could write the following instead?

'someString'.isValidEmail;

Extension to the rescue! ⛑

Instead of defining a util class, you can define an extension which will be applied on a certain type. Then simply use this to obtain the current instance as if you were inside a regular class member.

Most of the time, you'll create property extensions instead of methods. While static util methods need an instance to be passed in, extensions have access to the instance with the this keyword.

main.dart

extension StringExtensions on String {
  bool get isValidEmail {
    final emailRegExp = RegExp(r"^[a-zA-Z0-9.][email protected][a-zA-Z0-9]+\.[a-zA-Z]+");
    return emailRegExp.hasMatch(this);
  }
}

// Usage
main() {
  'someString'.isValidEmail;
}

More kinds of extensions

Obviously, extension methods are supported as well. What's a cool though, is that you can write operator extensions! Let's create two identical string extensions for concatenating with a space:

main.dart

extension StringExtensions on String {
  String concatWithSpace(String other) {
    return '$this $other';
  }

  /// DOCUMENTATION IS SUPPORTED: Concatenates two strings with a space in between.
  String operator &(String other) => '$this $other';
}

Using these is straightforward. While I wouldn't recommend creating these kinds of silly operators, they may come in handy with some classes.

main.dart

main() {
  'one'.concatWithSpace('two');
  'one' & 'two';
}

Issues with inheritance ⚠

Let's imagine you want to add extensions to an int. Of course, doing so is simple...

main.dart

extension IntExtensions on int {
  int addTen() => this + 10;
}

But then you realize that you also want to have an extension on a double doing basically the same thing. So... is code duplication unavoidable?

main.dart

extension DoubleExtensions on double {
  double addTen() => this + 10;
}

Of course that duplication can be avoided! After all, double and int are subclasses of num. Let's define an extension for the base class and call it a day, right?

main.dart

extension NumExtensions on num {
  num addTen() => this + 10;
}

We've accomplished one thing - all subclasses of num now have the addTen extension. But... no matter if we invoke it on an int or on a double, it always returns num! This impacts compile-time error checking big time:

main.dart

main() {
  int anInt = 1.addTen();
  // Run-time error!
  // Putting a 'num' which is really a 'double' into an 'int' variable
  int shouldBeDouble = 1.0.addTen();
}
Defining extensions for base classes can lead to run-time errors which are such as the following TypeError: "type 'double' is not a subtype of type 'int'".

What if the return type of the extension method could be more specific? Behold then, because generic extensions are coming to the rescue! 

Generic extensions

It turns out that specifying a generic constraint on a type parameter solves all the deficiencies described above. Namely, we'll get compile-time errors if we mess types up, which is a good thing.

The following extension will add the addTen method to every type fulfilling the generic constraint (every subclass).

main.dart

extension NumGenericExtensions<T extends num> on T {
  T addTen() => this + 10;
}

Generics then work as expected, not allowing the following code to even compile!

main.dart

main() {
  // Compile-time error!
  int shouldBeDouble = 1.0.addTen();
}

What you learned

Extension members are a powerful new feature of the Dart language. You learned how to create extension properties, extension methods and even extension operators. You also saw how solving code duplication by defining an extension on the base class may not always be the best option. In most cases, you should use generic extensions instead.

Matej Rešetár
 

Matej is an app developer with a knack for teaching others. If he's not programming, making tutorials or doing other business, he's mostly working out, listening to audiobooks and taking cold showers.

>