Dart extension methods code shown on a laptop screen

Dart Extension Methods for Beginners: Add to Any Type

Sooner or later every Dart beginner writes a little helper like capitalize(myString) or average(myList), and then calls it a hundred times across a project. It works, but it reads backwards. You have to lead with the function name instead of the thing you actually care about. Dart extension methods flip that around. They let you add your own methods to a type you didn’t write, so you can call myString.capitalize() the same way you call myString.toUpperCase(). By the end of this post you’ll be able to add methods, getters, and even operators to String, int, List, and any other type, without touching the original class and without subclassing anything.

What Dart extension methods actually do

An extension bolts new behavior onto an existing type. You are not editing the String class and you are not making a subclass of it. You are telling the compiler, “when someone calls this method on a String, here’s what to run.” That’s the whole idea.

Here’s the smallest useful example. Say you want to capitalize the first letter of a word. The core library doesn’t ship a capitalize method, so we add one.

extension StringCasing on String {
  String capitalize() {
    if (isEmpty) return this;
    return this[0].toUpperCase() + substring(1);
  }
}

void main() {
  print('hello'.capitalize());
  print('dart'.capitalize());
}

Run that and you’ll see:

Hello
Dart

You might wonder why not just write a plain capitalize(text) function, or make a subclass. A helper function works, but it reads inside out and it doesn’t show up when your editor autocompletes on a String, so you forget it exists. A subclass is heavier than the problem: you’d have to construct your special String type everywhere instead of using the ones Dart already hands you. An extension keeps the type you already have and just teaches it one new trick, and your editor will suggest it right alongside the built-in methods.

Read the declaration slowly, because the shape is the part that trips people up. extension StringCasing on String says “I’m adding to String, and I’m naming this group of additions StringCasing.” Inside the braces, this refers to whatever String the method was called on, so this[0] is the first character and substring(1) is everything after it. You can call isEmpty and substring with no prefix because you’re already inside the String’s world. The name StringCasing is optional, but naming your extensions is a good habit, and you’ll see in a minute why it matters.

Laptop on a desk showing Dart code that extends existing classes
An extension teaches an existing type one new trick without subclassing it.

Extensions can add getters, not just methods

Methods are the obvious use, but an extension can also add a getter, which is a value you read without parentheses. If you’ve followed the Dart string methods post, this is the natural next step: instead of a loose helper function, you get a property that looks like it was built into the type.

extension StringChecks on String {
  bool get isValidEmail => contains('@') && split('@').last.contains('.');
  int get wordCount => trim().isEmpty ? 0 : trim().split(RegExp(r's+')).length;
}

void main() {
  print('hi@site.com'.isValidEmail);
  print('nope'.isValidEmail);
  print('learn dart the slow way'.wordCount);
}

Output:

true
false
5

Notice there are no parentheses after isValidEmail or wordCount. That’s the difference between a getter and a method. You use a getter when the thing feels like a property of the value (“is this a valid email,” “how many words is this”), and a method when it feels like an action (“capitalize this,” “reverse this”). If you’ve read the Dart getters and setters post, the same instinct applies here. And to be clear about that email check: it’s a teaching example, not production email validation. Real email rules are messy. For a form, a quick contains check plus actually sending a confirmation is the honest approach.

Extending collections and nullable types

Extensions get genuinely useful once you put them on List. Say you’re averaging quiz scores in a dozen different widgets. Instead of copying an averaging loop everywhere, add it to the list type once.

extension IntListStats on List<int> {
  int get total => isEmpty ? 0 : reduce((a, b) => a + b);
  double average() => isEmpty ? 0.0 : total / length;
}

void main() {
  final scores = [88, 92, 79, 95];
  print(scores.total);
  print(scores.average());
}

Output:

354
88.5

This is where extensions start to feel like a superpower. scores.average() reads exactly like something Dart shipped with, and the averaging logic lives in one place. If you want a refresher on the list methods this builds on, the Dart list methods post covers reduce and friends.

Extensions can be generic too, which is what makes them scale. If you learned type parameters in the Dart generics post, you can put one on an extension and have it work for a list of anything. Here’s a secondOrNull getter that returns the second item, or null when there isn’t one, no matter what the list holds.

extension ListSafe<T> on List<T> {
  T? get secondOrNull => length >= 2 ? this[1] : null;
}

void main() {
  print([88, 92, 79, 95].secondOrNull);
  print(<int>[].secondOrNull);
}

Output:

92
null

The <T> after the extension name is the same type parameter idea you’d use on a class. It means “whatever type this list holds, that’s what a safe lookup returns.” Write it once and it covers a list of ints, a list of strings, a list of your own objects, all of them.

You can also extend a nullable type, which is handy once you’re working with null safety. Put the ? right in the on clause and the extension applies even when the value is null.

extension StringOrEmpty on String? {
  String orEmpty() => this ?? '';
}

void main() {
  String? maybeName;
  print(maybeName.orEmpty().length);
  print('Michelle'.orEmpty());
}

Output:

0
Michelle

Calling a method on something that might be null usually makes Dart nervous, but because the extension is declared on String?, this one is allowed. If nullable types still feel shaky, the Dart null safety post is worth a detour before you lean on this pattern.

Close-up of a code editor writing a Dart String extension method
Once an extension is imported, your editor suggests it right next to the built-in methods.

Common mistakes with extension methods

A few things trip up almost every first-timer, and they’re easier to spot once you know they exist.

First, extensions are resolved at compile time based on the static type, not at runtime. If you store your String in a variable typed as Object, your String extension won’t be found, because as far as the compiler knows the value is just an Object. Keep the specific type visible and you’ll be fine.

Second, an extension only works where it’s imported. If you define StringCasing in one file and call .capitalize() in another, you have to import the file that declares it. The method isn’t part of String globally; it travels with the file it lives in.

Third, you can’t add fields. An extension can add methods, getters, setters, and operators, but it can’t add new instance variables to hold state. There’s nowhere on the original object to store them. If you need to stash data, you want a wrapper class or composition, not an extension.

Fourth, watch for name clashes. If two imported extensions both add a capitalize method to String, Dart won’t guess which one you meant. That’s exactly why you name your extensions: you can write StringCasing('hello').capitalize() to say which one you want. And if the type ever gains a real method with the same name, the real method wins and your extension quietly stops being called.

Where to go next

You’ve now seen the full shape of Dart extension methods: add a method to any type with extension Name on Type, add getters for values that feel like properties, extend collections and nullable types, and sidestep the four mistakes that catch beginners. The payoff is code that reads in the order you think (“this score list, averaged”) instead of inside out (“average, of this score list”).

Try this as a small exercise. Write an extension on int that adds a getter called doubled (returns the number times two) and a method called times that runs a function that many times, so 3.times(() => print('hi')) prints hi three times. It’s a short file, and it will lock in the pattern better than reading ever could. Pick doubled rather than something like isEven on purpose, since int already ships isEven and a real member always wins over an extension. When you’re ready for the official rules on where extensions can and can’t reach, the Dart language docs on extension methods are the reference to keep open. Add one small extension to your next project and you’ll reach for them constantly after that.

Leave a Comment

Your email address will not be published. Required fields are marked *