Watch the video (12 min)
On this page24 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What Is an Extension?
  5. 2. Your First Extension
  6. 3. this Inside an Extension
  7. 4. Getters: int.isPrime and List<int>.average
  8. 5. Operators in an Extension
  9. 6. What an Extension Cannot Do
  10. 7. Extensions on Nullable Types
  11. 8. Extensions on Enums
  12. 9. Generic Extensions: A Preview
  13. 10. Extensions Are Resolved by the Static Type
  14. 11. Name Conflicts and Explicit Application
  15. 12. A Real-World Example
  16. Code Examples
  17. Common Beginner Mistakes
  18. Best Practices
  19. Mini Challenge
  20. Practice Questions
  21. Practice Answers
  22. Quick Revision
  23. Key Takeaways
  24. What's Next?

Lecture 69 — Extension Methods

Category11 — Enums, Extensions & Typedefs
Lecture069 of 140
Estimated video length12 min
TargetDart 3.12.2
LevelIntermediate

You cannot edit String, int or List — they belong to Dart. With an extension, you can still give them new methods, getters and operators, and call them as if they were built in.

Contents

Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways

Every error message in this lecture is real. Each one was produced by running the code shown on Dart 3.12.2, so you will recognize it when you meet it yourself.


Prerequisites

Students should already understand, from Lectures 008–068:

  • string methods such as toUpperCase, substring and replaceAll (Lecture 008)
  • dynamic versus Object (Lecture 011) and nullable types with ?? (Lectures 013–014)
  • operators defined inside a class (Lecture 021) and switch expressions (Lecture 025)
  • fold on lists (Lecture 044), and this and getters (Lecture 056)
  • enums (Lectures 067–068)

Connection With Previous Lectures

In Lecture 044 we found that a list of marks has no sum:

dart
void main() {
  var marks = [80, 75, 85];
  print(marks.sum); // ✗
}
output
Error: The getter 'sum' isn't defined for the type 'List<int>'.

We worked around it by writing fold(0, (total, mark) => total + mark) every time. The same happens with a name: there is no 'ali'.capitalize(). We could write a top-level function capitalize('ali'), but that reads backwards. We cannot open List or String and add the member ourselves — those classes belong to Dart. We need a way to add members to a type without changing it.


Learning Objectives

By the end of this lecture you will be able to:

  • Explain what an extension is and when it helps
  • Declare an extension with extension Name on Type
  • Add methods, getters and operators to existing types
  • Use this inside an extension
  • Write extensions on nullable types and on enums
  • Recognize why extensions do not work on dynamic values
  • Resolve name conflicts with an explicit extension application
  • Recognize what an extension cannot do

1. What Is an Extension?

An extension adds new members — methods, getters, setters, operators — to a type that already exists. The type itself is not changed; Dart simply lets you call the new members on its values.

Analogy — think of a clip-on lens for a phone camera. You do not open the phone or rebuild it; you clip something on, and the phone can suddenly do more.

The new members are called extension methods, even when they are getters or operators.


2. Your First Extension

The form is extension Name on Type { ... }, written at the top level of the file:

dart
extension StringTools on String {
  String capitalize() {
    return '${this[0].toUpperCase()}${substring(1)}';
  }
}

void main() {
  print('ali'.capitalize());
  print('sara'.capitalize());
}
output
Ali
Sara

StringTools is the extension's name and String is the type it extends. Every String value can now call capitalize() with a dot, exactly like toUpperCase().


3. this Inside an Extension

Inside an extension, this is the value the member was called on. In 'ali'.capitalize(), this is 'ali'. As in a class (Lecture 056), this. is optional when calling the type's own members — substring(1) above means this.substring(1):

dart
extension StringTools on String {
  String get firstLetter => this[0];
  int get wordCount => split(' ').length;
}

void main() {
  print('Zayan'.firstLetter);
  print('Dart is fun'.wordCount);
}
output
Z
3

this[0] needs this, because [0] alone would be a list. split(' ') works without it.


4. Getters: int.isPrime and List<int>.average

Getters work just as they do in a class — no parentheses when you read them:

dart
extension NumberTools on int {
  bool get isPrime {
    if (this < 2) return false;
    for (var i = 2; i * i <= this; i++) {
      if (this % i == 0) return false;
    }
    return true;
  }
}

extension MarksTools on List<int> {
  int get sum => fold(0, (total, mark) => total + mark);
  double get average => isEmpty ? 0 : sum / length;
}

void main() {
  print(7.isPrime);
  print(9.isPrime);
  var marks = [80, 75, 85];
  print(marks.sum);
  print(marks.average);
}
output
true
false
240
80.0

The missing sum from Lecture 044 now exists — written once, used everywhere. MarksTools extends only List<int>; a List<String> does not get sum.


5. Operators in an Extension

In Lecture 021, a class defined its own + with operator. An extension can do the same for a type it does not own. Here - removes every copy of some text:

dart
extension TextMath on String {
  String operator -(String part) => replaceAll(part, '');
}

void main() {
  print('banana' - 'a');
  print('1-800-555' - '-');
}
output
bnn
1800555

Use this with care: an operator should mean something the reader would guess.


6. What an Extension Cannot Do

An extension adds behaviour, not storage. It cannot declare instance fields:

dart
extension Counter on String {
  int count = 0; // ✗
}
output
Error: Extensions can't declare instance fields
Try removing the field declaration or making it a static field

An extension also cannot replace a member the type already has. If String already has length, an extension getter called length is simply ignored — the real one always wins:

dart
extension Broken on String {
  int get length => 0;
}

void main() {
  print('ali'.length);
}
output
3

There is no error and no warning, so pick names the type does not already use.


7. Extensions on Nullable Types

An extension on String cannot be called on a String?. An extension on String? can — even when the value is null, and without ?.:

dart
extension NullTools on String? {
  bool get isNullOrEmpty => (this ?? '').isEmpty;
  String orDefault(String fallback) => this ?? fallback;
}

void main() {
  String? nickname;
  print(nickname.isNullOrEmpty);
  print(nickname.orDefault('No nickname'));
  nickname = 'Zee';
  print(nickname.orDefault('No nickname'));
}
output
true
No nickname
Zee

Here this may be null, so treat it like any nullable value. Note that this is never promoted: after this == null ||, writing this.isEmpty is still an error. That is why the getter uses ??.


8. Extensions on Enums

An extension works on any type, including an enum. In Lecture 068 you added members inside an enum you wrote yourself. An extension is the tool when you cannot edit the enum, or want to keep a helper separate from it:

dart
enum OrderStatus { pending, shipped, delivered }

extension StatusText on OrderStatus {
  String get label => switch (this) {
    OrderStatus.pending => 'Waiting to ship',
    OrderStatus.shipped => 'On the way',
    OrderStatus.delivered => 'Arrived',
  };
  bool get isDone => this == OrderStatus.delivered;
}

void main() {
  var status = OrderStatus.shipped;
  print(status.label);
  print(status.isDone);
}
output
On the way
false

9. Generic Extensions: A Preview

MarksTools worked only on List<int>. An extension can also work on a list of any type, with a type placeholder written <T>:

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

void main() {
  print(['Ali', 'Sara'].secondOrNull);
  print([42].secondOrNull);
}
output
Sara
null

T stands for "whatever the element type is". Read it, run it, and don't worry about it yet — we will study generics properly in Lecture 072 — Generics Fundamentals.


10. Extensions Are Resolved by the Static Type

Dart chooses an extension while compiling, by looking at the variable's declared type. A dynamic variable has no type to look at, so no extension is ever found — and because dynamic turns checking off (Lecture 011), the mistake only shows up when the program runs:

dart
extension StringTools on String {
  String capitalize() => '${this[0].toUpperCase()}${substring(1)}';
}

void main() {
  dynamic name = 'sara';
  print(name.capitalize());
}
output
Unhandled exception:
NoSuchMethodError: Class 'String' has no instance method 'capitalize'.
Receiver: "sara"
Tried calling: capitalize()

The value really is a String, but the extension is not part of String — it was only ever attached to things the compiler knows are strings. With Object, the compiler catches it:

dart
void main() {
  Object name = 'sara';
  // print(name.capitalize()); // ✗
}
output
Error: The method 'capitalize' isn't defined for the type 'Object'.

The fix for both is a real type: String name = 'sara'; or (name as String).capitalize().


11. Name Conflicts and Explicit Application

Two extensions may add a member with the same name to the same type. Dart then refuses to guess:

dart
extension StringTools on String {
  String capitalize() => '${this[0].toUpperCase()}${substring(1)}';
}

extension LoudTools on String {
  String capitalize() => toUpperCase();
}

void main() {
  // print('ali'.capitalize()); // ✗
}
output
Error: The method 'capitalize' is defined in multiple extensions for 'String' and neither is more specific.
Try using an explicit extension application of the wanted extension or hiding unwanted extensions from scope.

An explicit extension application names the extension and wraps the value: Name(value).member().

dart
extension StringTools on String {
  String capitalize() => '${this[0].toUpperCase()}${substring(1)}';
}

extension LoudTools on String {
  String capitalize() => toUpperCase();
}

void main() {
  print(StringTools('ali').capitalize());
  print(LoudTools('ali').capitalize());
}
output
Ali
ALI

This is also why extensions have names. "Hiding" an extension from scope uses imports, which we meet in Lecture 101 — Dart Libraries.


12. A Real-World Example

dart
extension MarksTools on List<int> {
  int get sum => fold(0, (total, mark) => total + mark);
  double get average => isEmpty ? 0 : sum / length;
}

extension GradeTools on double {
  String get grade => this >= 80 ? 'A' : (this >= 60 ? 'B' : 'C');
}

extension NameTools on String? {
  String get display {
    final name = this ?? 'unknown';
    return '${name[0].toUpperCase()}${name.substring(1)}';
  }
}

void main() {
  String? name = 'sara';
  var marks = [82, 74, 90];
  print('Student: ${name.display}');
  print('Total: ${marks.sum}');
  print('Average: ${marks.average.toStringAsFixed(1)}');
  print('Grade: ${marks.average.grade}');
}
output
Student: Sara
Total: 246
Average: 82.0
Grade: A

Every line reads like plain English: marks.sum, marks.average.grade, name.display. None of List, double or String was changed.


Code Examples

Example 1 — Capitalize a Name

dart
extension StringTools on String {
  String capitalize() => '${this[0].toUpperCase()}${substring(1)}';
}

void main() {
  print('zayan'.capitalize());
}
output
Zayan

Example 2 — A Getter on int

dart
extension NumberTools on int {
  bool get isPrime {
    if (this < 2) return false;
    for (var i = 2; i * i <= this; i++) {
      if (this % i == 0) return false;
    }
    return true;
  }
}

void main() {
  for (var n = 1; n <= 10; n++) {
    if (n.isPrime) print(n);
  }
}
output
2
3
5
7

Example 3 — A Method with a Parameter

dart
extension Shorten on String {
  String shorten(int max) => length <= max ? this : '${substring(0, max)}...';
}

void main() {
  print('Programming'.shorten(7));
  print('Dart'.shorten(7));
}
output
Program...
Dart

Example 4 — An Extension on double

dart
extension Money on double {
  String get asRupees => 'Rs ${toStringAsFixed(2)}';
}

void main() {
  print(1250.5.asRupees);
}
output
Rs 1250.50

Example 5 — A Nullable Receiver

dart
extension OrDash on String? {
  String get orDash => this ?? '-';
}

void main() {
  String? phone;
  print(phone.orDash);
  phone = '0300-1234567';
  print(phone.orDash);
}
output
-
0300-1234567

Example 6 — Explicit Application

dart
extension StringTools on String {
  String capitalize() => '${this[0].toUpperCase()}${substring(1)}';
}

void main() {
  print(StringTools('ali').capitalize());
}
output
Ali

With no conflict, 'ali'.capitalize() would do the same; the explicit form is for when two extensions clash.

Example 7 — Student Marks

dart
extension MarksTools on List<int> {
  int get sum => fold(0, (total, mark) => total + mark);
  double get average => isEmpty ? 0 : sum / length;
  int get passed => where((mark) => mark >= 50).length;
}

void main() {
  var marks = [45, 80, 62, 38, 90];
  print('Total: ${marks.sum}');
  print('Average: ${marks.average}');
  print('Passed: ${marks.passed} of ${marks.length}');
}
output
Total: 315
Average: 63.0
Passed: 3 of 5

How it works

  1. sum uses fold, as in Lecture 044 — but now it lives in one place.
  2. average reuses sum; inside an extension, members can call each other.
  3. passed counts with where(...).length.

Example 8 — Shopping Total

dart
extension CartTools on Map<String, int> {
  int get total => values.fold(0, (sum, price) => sum + price);
  String get mostExpensive =>
      entries.reduce((a, b) => a.value >= b.value ? a : b).key;
}

void main() {
  var cart = {'Notebook': 500, 'Bag': 1500, 'Pen': 50};
  print('Total: ${cart.total}');
  print('Most expensive: ${cart.mostExpensive}');
}
output
Total: 2050
Most expensive: Bag

How it works

  1. The extension is on Map<String, int>, so values and entries are the map's own members.
  2. total folds the prices; mostExpensive keeps the larger entry and returns its key.

Example 9 — Bank Balance

dart
extension Transactions on List<double> {
  double get balance => fold(0.0, (sum, t) => sum + t);
  double get deposits => where((t) => t > 0).fold(0.0, (sum, t) => sum + t);
}

void main() {
  var history = [1000.0, -250.0, 500.0, -120.5];
  print('Deposits: ${history.deposits}');
  print('Balance: ${history.balance}');
}
output
Deposits: 1500.0
Balance: 1129.5

How it works

  1. Positive values are deposits, negative values are withdrawals.
  2. fold(0.0, ...) starts from a double, so the results are double (Lecture 044).

Example 10 — Temperature

dart
extension Temperature on double {
  double get toFahrenheit => this * 9 / 5 + 32;
  String get feeling => this >= 30 ? 'Hot' : (this >= 15 ? 'Mild' : 'Cold');
}

void main() {
  var celsius = 35.0;
  print('${celsius.toFahrenheit} F');
  print(celsius.feeling);
}
output
95.0 F
Hot

How it works

  1. this is the temperature in Celsius, 35.0.
  2. toFahrenheit is a getter, so it is read without parentheses.

Common Beginner Mistakes

1. Calling an extension on a dynamic value

dart
void main() {
  dynamic name = 'sara';
  // print(name.capitalize()); // ✗ compiles, crashes at run time
}
output
NoSuchMethodError: Class 'String' has no instance method 'capitalize'.

Give the variable a real type, or cast: (name as String).capitalize().

2. Declaring a field in an extension

dart
extension Counter on String {
  // int count = 0; // ✗
}
output
Error: Extensions can't declare instance fields

Calculate the value in a getter instead.

3. Reusing a name the type already has

An extension length on String is never called — the real length wins, silently. Choose a new name.

4. Writing this.member after a null check in a nullable extension

dart
extension NullTools on String? {
  // bool get isNullOrEmpty => this == null || this.isEmpty; // ✗
}
output
Error: Property 'isEmpty' cannot be accessed on 'String?' because it is potentially null.

this is never promoted. Use (this ?? '').isEmpty.

5. Two extensions with the same member

output
Error: The method 'capitalize' is defined in multiple extensions for 'String' and neither is more specific.

Use StringTools('ali').capitalize() to say which one you mean.


Best Practices

  • Use an extension when a helper clearly belongs to a type you cannot edit.
  • For your own classes and enums, prefer adding the member directly.
  • Give extensions descriptive names such as StringTools — you need them for explicit application.
  • Choose member names the type does not already have.
  • Use operators only when their meaning is obvious.
  • Keep variables strongly typed; extensions never work through dynamic.

Mini Challenge

Write an extension GradeTools on int with a getter grade that returns 'A' for 80 and above, 'B' for 60 and above, and 'C' otherwise. Print the grade of Ali (85), Sara (72) and Zayan (40).

Your program should print exactly:

output
Ali: A
Sara: B
Zayan: C

Try it yourself before opening the solution.

Show solution
dart
extension GradeTools on int {
  String get grade {
    if (this >= 80) return 'A';
    if (this >= 60) return 'B';
    return 'C';
  }
}

void main() {
  print('Ali: ${85.grade}');
  print('Sara: ${72.grade}');
  print('Zayan: ${40.grade}');
}
output
Ali: A
Sara: B
Zayan: C

How it works

  1. this is the mark the getter was read on.
  2. Each return ends the getter, so later checks only run for lower marks.

Practice Questions

Work through these in order — they run from beginner to application to challenge.

Beginner

  1. Write an extension on String with a method shout() that returns the text in capitals followed by !. Print 'hello'.shout().
  2. Write an extension on int with a getter squared. Print 7.squared.
  3. Write an extension on int with a getter isAdult (18 or more). Print 17.isAdult and 18.isAdult.

Application

  1. Write an extension on List<int> with a getter failed that counts marks below 50. Print it for [45, 60, 72, 30].

  2. Write an extension on String? with a getter orUnknown that returns 'Unknown' for null. Print it for a null name and for 'Sara'.

  3. This program compiles but crashes. Explain why, and fix it by changing one word:

    dart
    extension StringTools on String {
      String capitalize() => '${this[0].toUpperCase()}${substring(1)}';
    }
    
    void main() {
      dynamic city = 'lahore';
      print(city.capitalize());
    }
  4. Write enum Size { small, medium, large } and an extension on it with a getter price returning 200, 350 and 500. Print the price of each size with a for-in loop over Size.values.

Challenge

  1. Two extensions on String both define initial(): Short returns the first letter, and Dotted returns the first letter in capitals followed by .. Print both for 'ali'.
  2. Write an extension on List<double> with getters highest (using reduce) and average. Print both for the temperatures [31.5, 28.0, 35.5].
  3. Write an extension on List<int> with an operator * that multiplies every element by an int. Print [1, 2, 3] * 2.

Practice Answers

Question 1 — Solution

dart
extension Loud on String {
  String shout() => '${toUpperCase()}!';
}

void main() {
  print('hello'.shout());
}
output
HELLO!

Question 2 — Solution

dart
extension Square on int {
  int get squared => this * this;
}

void main() {
  print(7.squared);
}
output
49

Question 3 — Solution

dart
extension Age on int {
  bool get isAdult => this >= 18;
}

void main() {
  print(17.isAdult);
  print(18.isAdult);
}
output
false
true

Question 4 — Solution

dart
extension MarksTools on List<int> {
  int get failed => where((mark) => mark < 50).length;
}

void main() {
  print([45, 60, 72, 30].failed);
}
output
2

Question 5 — Solution

dart
extension OrUnknown on String? {
  String get orUnknown => this ?? 'Unknown';
}

void main() {
  String? name;
  print(name.orUnknown);
  name = 'Sara';
  print(name.orUnknown);
}
output
Unknown
Sara

Question 6 — Solution

Extensions are chosen by the static type. city is dynamic, so no extension is found and the call fails at run time with NoSuchMethodError. Change dynamic to String:

dart
extension StringTools on String {
  String capitalize() => '${this[0].toUpperCase()}${substring(1)}';
}

void main() {
  String city = 'lahore';
  print(city.capitalize());
}
output
Lahore

Question 7 — Solution

dart
enum Size { small, medium, large }

extension SizePrice on Size {
  int get price => switch (this) {
    Size.small => 200,
    Size.medium => 350,
    Size.large => 500,
  };
}

void main() {
  for (var size in Size.values) {
    print('${size.name}: ${size.price}');
  }
}
output
small: 200
medium: 350
large: 500

Question 8 — Solution

dart
extension Short on String {
  String initial() => this[0];
}

extension Dotted on String {
  String initial() => '${this[0].toUpperCase()}.';
}

void main() {
  print(Short('ali').initial());
  print(Dotted('ali').initial());
}
output
a
A.

'ali'.initial() would be the "defined in multiple extensions" error.

Question 9 — Solution

dart
extension Readings on List<double> {
  double get highest => reduce((a, b) => a > b ? a : b);
  double get average => fold(0.0, (sum, t) => sum + t) / length;
}

void main() {
  var temperatures = [31.5, 28.0, 35.5];
  print(temperatures.highest);
  print(temperatures.average);
}
output
35.5
31.666666666666668

Question 10 — Solution

dart
extension Scale on List<int> {
  List<int> operator *(int factor) => map((n) => n * factor).toList();
}

void main() {
  print([1, 2, 3] * 2);
}
output
[2, 4, 6]

Quick Revision

  1. What does an extension add to a type, and what does it leave unchanged?
  2. What is the general form of an extension declaration?
  3. What does this mean inside an extension?
  4. Why can an extension on String? be called on a null value?
  5. Why does an extension method fail on a dynamic variable?
  6. What happens if an extension member has the same name as a real member of the type?
  7. How do you choose between two extensions that define the same member?
  8. Name one thing an extension cannot declare.

Key Takeaways

  • An extension adds methods, getters, setters and operators to an existing type without changing it.
  • Syntax: extension Name on Type { ... }; members are called with a dot like built-in ones.
  • this is the value the member was called on; this. is optional for the type's own members.
  • An extension on String? works on null values; this is never promoted, so use ??.
  • Extensions work on any type — core types, your classes, enums, and (with <T>) generic types.
  • Extensions are chosen by the static type: on dynamic they are never found.
  • A real member always beats an extension member with the same name.
  • Clashing extensions are resolved with Name(value).member().
  • Extensions cannot declare instance fields.

What's Next?

Lecture 70 — Extension Types. An extension adds members to a type, but the type stays the same: an int holding a student ID is still just an int, and nothing stops you from mixing it up with a price. Next we will learn how extension types wrap an existing value in a brand-new type, with its own members and its own type checks, at no run-time cost.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.