On this page24 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. What Is an Extension?
- 2. Your First Extension
- 3. this Inside an Extension
- 4. Getters: int.isPrime and List<int>.average
- 5. Operators in an Extension
- 6. What an Extension Cannot Do
- 7. Extensions on Nullable Types
- 8. Extensions on Enums
- 9. Generic Extensions: A Preview
- 10. Extensions Are Resolved by the Static Type
- 11. Name Conflicts and Explicit Application
- 12. A Real-World Example
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 69 — Extension Methods
| Category | 11 — Enums, Extensions & Typedefs |
| Lecture | 069 of 140 |
| Estimated video length | 12 min |
| Target | Dart 3.12.2 |
| Level | Intermediate |
You cannot edit
String,intorList— 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,substringandreplaceAll(Lecture 008) dynamicversusObject(Lecture 011) and nullable types with??(Lectures 013–014)- operators defined inside a class (Lecture 021) and switch expressions (Lecture 025)
foldon lists (Lecture 044), andthisand getters (Lecture 056)- enums (Lectures 067–068)
Connection With Previous Lectures
In Lecture 044 we found that a list of marks has no sum:
void main() {
var marks = [80, 75, 85];
print(marks.sum); // ✗
}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
thisinside an extension - Write extensions on nullable types and on enums
- Recognize why extensions do not work on
dynamicvalues - 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:
extension StringTools on String {
String capitalize() {
return '${this[0].toUpperCase()}${substring(1)}';
}
}
void main() {
print('ali'.capitalize());
print('sara'.capitalize());
}Ali
SaraStringTools 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):
extension StringTools on String {
String get firstLetter => this[0];
int get wordCount => split(' ').length;
}
void main() {
print('Zayan'.firstLetter);
print('Dart is fun'.wordCount);
}Z
3this[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:
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);
}true
false
240
80.0The 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:
extension TextMath on String {
String operator -(String part) => replaceAll(part, '');
}
void main() {
print('banana' - 'a');
print('1-800-555' - '-');
}bnn
1800555Use 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:
extension Counter on String {
int count = 0; // ✗
}Error: Extensions can't declare instance fields
Try removing the field declaration or making it a static fieldAn 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:
extension Broken on String {
int get length => 0;
}
void main() {
print('ali'.length);
}3There 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 ?.:
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'));
}true
No nickname
ZeeHere 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:
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);
}On the way
false9. 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>:
extension ListTools<T> on List<T> {
T? get secondOrNull => length > 1 ? this[1] : null;
}
void main() {
print(['Ali', 'Sara'].secondOrNull);
print([42].secondOrNull);
}Sara
nullT 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:
extension StringTools on String {
String capitalize() => '${this[0].toUpperCase()}${substring(1)}';
}
void main() {
dynamic name = 'sara';
print(name.capitalize());
}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:
void main() {
Object name = 'sara';
// print(name.capitalize()); // ✗
}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:
extension StringTools on String {
String capitalize() => '${this[0].toUpperCase()}${substring(1)}';
}
extension LoudTools on String {
String capitalize() => toUpperCase();
}
void main() {
// print('ali'.capitalize()); // ✗
}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().
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());
}Ali
ALIThis 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
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}');
}Student: Sara
Total: 246
Average: 82.0
Grade: AEvery 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
extension StringTools on String {
String capitalize() => '${this[0].toUpperCase()}${substring(1)}';
}
void main() {
print('zayan'.capitalize());
}ZayanExample 2 — A Getter on int
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);
}
}2
3
5
7Example 3 — A Method with a Parameter
extension Shorten on String {
String shorten(int max) => length <= max ? this : '${substring(0, max)}...';
}
void main() {
print('Programming'.shorten(7));
print('Dart'.shorten(7));
}Program...
DartExample 4 — An Extension on double
extension Money on double {
String get asRupees => 'Rs ${toStringAsFixed(2)}';
}
void main() {
print(1250.5.asRupees);
}Rs 1250.50Example 5 — A Nullable Receiver
extension OrDash on String? {
String get orDash => this ?? '-';
}
void main() {
String? phone;
print(phone.orDash);
phone = '0300-1234567';
print(phone.orDash);
}-
0300-1234567Example 6 — Explicit Application
extension StringTools on String {
String capitalize() => '${this[0].toUpperCase()}${substring(1)}';
}
void main() {
print(StringTools('ali').capitalize());
}AliWith no conflict, 'ali'.capitalize() would do the same; the explicit form is for when two
extensions clash.
Example 7 — Student Marks
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}');
}Total: 315
Average: 63.0
Passed: 3 of 5How it works
sumusesfold, as in Lecture 044 — but now it lives in one place.averagereusessum; inside an extension, members can call each other.passedcounts withwhere(...).length.
Example 8 — Shopping Total
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}');
}Total: 2050
Most expensive: BagHow it works
- The extension is
on Map<String, int>, sovaluesandentriesare the map's own members. totalfolds the prices;mostExpensivekeeps the larger entry and returns its key.
Example 9 — Bank Balance
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}');
}Deposits: 1500.0
Balance: 1129.5How it works
- Positive values are deposits, negative values are withdrawals.
fold(0.0, ...)starts from adouble, so the results aredouble(Lecture 044).
Example 10 — Temperature
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);
}95.0 F
HotHow it works
thisis the temperature in Celsius,35.0.toFahrenheitis a getter, so it is read without parentheses.
Common Beginner Mistakes
1. Calling an extension on a dynamic value
void main() {
dynamic name = 'sara';
// print(name.capitalize()); // ✗ compiles, crashes at run time
}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
extension Counter on String {
// int count = 0; // ✗
}Error: Extensions can't declare instance fieldsCalculate 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
extension NullTools on String? {
// bool get isNullOrEmpty => this == null || this.isEmpty; // ✗
}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
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:
Ali: A
Sara: B
Zayan: CTry it yourself before opening the solution.
Show solution
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}');
}Ali: A
Sara: B
Zayan: CHow it works
thisis the mark the getter was read on.- Each
returnends 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
- Write an extension on
Stringwith a methodshout()that returns the text in capitals followed by!. Print'hello'.shout(). - Write an extension on
intwith a gettersquared. Print7.squared. - Write an extension on
intwith a getterisAdult(18 or more). Print17.isAdultand18.isAdult.
Application
-
Write an extension on
List<int>with a getterfailedthat counts marks below50. Print it for[45, 60, 72, 30]. -
Write an extension on
String?with a getterorUnknownthat returns'Unknown'fornull. Print it for anullname and for'Sara'. -
This program compiles but crashes. Explain why, and fix it by changing one word:
extension StringTools on String { String capitalize() => '${this[0].toUpperCase()}${substring(1)}'; } void main() { dynamic city = 'lahore'; print(city.capitalize()); } -
Write
enum Size { small, medium, large }and an extension on it with a getterpricereturning200,350and500. Print the price of each size with afor-inloop overSize.values.
Challenge
- Two extensions on
Stringboth defineinitial():Shortreturns the first letter, andDottedreturns the first letter in capitals followed by.. Print both for'ali'. - Write an extension on
List<double>with gettershighest(usingreduce) andaverage. Print both for the temperatures[31.5, 28.0, 35.5]. - Write an extension on
List<int>with an operator*that multiplies every element by anint. Print[1, 2, 3] * 2.
Practice Answers
Question 1 — Solution
extension Loud on String {
String shout() => '${toUpperCase()}!';
}
void main() {
print('hello'.shout());
}HELLO!Question 2 — Solution
extension Square on int {
int get squared => this * this;
}
void main() {
print(7.squared);
}49Question 3 — Solution
extension Age on int {
bool get isAdult => this >= 18;
}
void main() {
print(17.isAdult);
print(18.isAdult);
}false
trueQuestion 4 — Solution
extension MarksTools on List<int> {
int get failed => where((mark) => mark < 50).length;
}
void main() {
print([45, 60, 72, 30].failed);
}2Question 5 — Solution
extension OrUnknown on String? {
String get orUnknown => this ?? 'Unknown';
}
void main() {
String? name;
print(name.orUnknown);
name = 'Sara';
print(name.orUnknown);
}Unknown
SaraQuestion 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:
extension StringTools on String {
String capitalize() => '${this[0].toUpperCase()}${substring(1)}';
}
void main() {
String city = 'lahore';
print(city.capitalize());
}LahoreQuestion 7 — Solution
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}');
}
}small: 200
medium: 350
large: 500Question 8 — Solution
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());
}a
A.'ali'.initial() would be the "defined in multiple extensions" error.
Question 9 — Solution
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);
}35.5
31.666666666666668Question 10 — Solution
extension Scale on List<int> {
List<int> operator *(int factor) => map((n) => n * factor).toList();
}
void main() {
print([1, 2, 3] * 2);
}[2, 4, 6]Quick Revision
- What does an extension add to a type, and what does it leave unchanged?
- What is the general form of an extension declaration?
- What does
thismean inside an extension? - Why can an extension
on String?be called on anullvalue? - Why does an extension method fail on a
dynamicvariable? - What happens if an extension member has the same name as a real member of the type?
- How do you choose between two extensions that define the same member?
- 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. thisis the value the member was called on;this.is optional for the type's own members.- An extension on
String?works onnullvalues;thisis 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
dynamicthey 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.