On this page21 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. The Problem: Two Kinds of int
- 2. Declaring an Extension Type
- 3. Mix-ups Become Errors
- 4. Adding Members
- 5. Constructors
- 6. Hiding vs Exposing the Representation
- 7. The Wrapper Disappears at Runtime
- 8. Extension Type vs Class vs Typedef
- 9. 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 70 — Extension Types
| Category | 11 — Enums, Extensions & Typedefs |
| Lecture | 070 of 140 |
| Estimated video length | 10 min |
| Target | Dart 3.12.2 |
| Level | Intermediate |
A user's ID and an order's ID are both just
int, so Dart lets you swap them by accident. An extension type gives each one its own type while it is being checked, at no runtime cost.
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 010–069:
- type checks and casts with
isandas, andruntimeType(Lecture 010) - classes, getters, and named, constant and
factoryconstructors (Lecture 055) implementsfrom Lecture 063- extension methods from Lecture 069
Connection With Previous Lectures
In Lecture 069, extension methods added new members to an existing type. They added to
every int, though. Sometimes we need the opposite: a few ints that must not behave like
the others.
void cancelOrder(int orderId) {
print('Cancelling order #$orderId');
}
void main() {
var userId = 7;
var orderId = 1042;
cancelOrder(userId);
}Cancelling order #7That's the wrong number, and Dart said nothing. Both values are int, so the type checker can't
tell them apart. A class wrapper would fix it, but that builds an extra object around every
number. Dart has a lighter tool.
Learning Objectives
By the end of this lecture you will be able to:
- Explain what an extension type is and what "zero-cost" means
- Declare an extension type with
extension type Name(Type field) - Use extension types to turn ID mix-ups into compile-time errors
- Add getters and methods to an extension type
- Write named and factory constructors for an extension type
- Choose between hiding the representation and exposing it with
implements - Recognize that the wrapper disappears at runtime, so
isandassee through it - Choose between an extension type, a class and a typedef
1. The Problem: Two Kinds of int
A representation is the plain value that holds the data, such as an int. Two very
different ideas, a user ID and an order ID, often share one representation. What we want is
a separate type for each, while the data stays a plain int.
Analogy: think of coloured sleeves on identical cards. A blue sleeve says "user", a red one says "order". The card inside is the same card. Only the sleeve tells you what it is.
2. Declaring an Extension Type
An extension type is declared with extension type, a name, and the representation in
parentheses:
extension type UserId(int id) {}
void main() {
var ali = UserId(7);
print(ali.id);
}7UserId(7) wraps the int 7, and ali.id reads it back. id is final, so it can't be
reassigned. The {} body is required, even when it's empty.
3. Mix-ups Become Errors
Give each kind of ID its own extension type, and Dart refuses to mix them:
extension type UserId(int id) {}
extension type OrderId(int id) {}
void showUser(UserId user) {
print('User #${user.id}');
}
void main() {
var ali = UserId(7);
var order = OrderId(7);
showUser(ali);
showUser(order);
showUser(7);
}Error: The argument type 'OrderId' can't be assigned to the parameter type 'UserId'.
showUser(order);
^
Error: The argument type 'int' can't be assigned to the parameter type 'UserId'.
showUser(7);
^Both IDs hold 7, yet only a real UserId is accepted. Even a bare int is rejected, so
every ID has to be wrapped on purpose.
4. Adding Members
The body can hold getters, methods and static members, built on the representation:
extension type Celsius(double value) {
double get fahrenheit => value * 9 / 5 + 32;
bool get isHot => value > 30;
String describe() => '$value°C';
}
void main() {
var today = Celsius(25);
print(today.describe());
print(today.fahrenheit);
print(today.isHot);
}25.0°C
77.0
falseAn extension type cannot have instance fields. The representation is its only data. That rule is what makes it free.
5. Constructors
UserId(7) uses the constructor created by the declaration. You can add named and
factory constructors, and mark the type const:
extension type const UserId(int id) {
UserId.guest() : id = 0;
factory UserId.fromText(String text) => UserId(int.parse(text));
bool get isGuest => id == 0;
}
void main() {
const admin = UserId(1);
var guest = UserId.guest();
var sara = UserId.fromText('42');
print('${admin.id} ${guest.isGuest} ${sara.id}');
}1 true 42A factory constructor can also clean a value before wrapping it. Here the primary
constructor is private (Marks._), so the only way in is through the factory:
extension type Marks._(int value) {
factory Marks(int value) => Marks._(value.clamp(0, 100));
}
void main() {
print(Marks(120).value);
print(Marks(-5).value);
print(Marks(88).value);
}100
0
88Rejecting bad values with an error needs exceptions, which we will study in Lecture 085 — throw & Custom Exceptions.
6. Hiding vs Exposing the Representation
By default, an extension type hides the members of its representation:
extension type UserId(int id) {}
void main() {
var ali = UserId(8);
print(ali.isEven);
}Error: The getter 'isEven' isn't defined for the type 'UserId'.
Try correcting the name to the name of an existing getter, or defining a getter or field named 'isEven'.
print(ali.isEven);
^^^^^^That's often what you want. Adding two user IDs makes no sense. When the full int API is
useful, write implements int:
extension type Score(int value) implements int {}
void main() {
var score = Score(40);
print(score + 5);
print(score.isEven);
int plain = score;
print(plain);
}45
true
40Now a Score can go anywhere an int can. But score + 5 gives a plain int, not a
Score. Wrap the result again if you need one.
7. The Wrapper Disappears at Runtime
"Zero-cost" means the extension type exists only while Dart checks your code. When the
program runs, a UserId simply is an int:
extension type UserId(int id) {}
extension type OrderId(int id) {}
void main() {
var ali = UserId(7);
print(ali);
print(ali.runtimeType);
print(ali is int);
Object? box = ali;
print(box is OrderId);
var order = box as OrderId;
print(order.id);
}7
int
true
true
7This is the important caveat. At runtime, is OrderId only asks "is it an int?", so a
UserId passes. The as cast works the same way, so it can't catch a mix-up either. An
extension type protects you while you write code, not once the code is running.
8. Extension Type vs Class vs Typedef
typedef StudentNumber = int;
extension type RollNumber(int value) {}
class Ticket {
final int value;
Ticket(this.value);
}
void main() {
StudentNumber a = 5;
int plain = a;
print(plain);
print(RollNumber(5) is int);
print(Ticket(5) is int);
}5
true
false| New type while checking? | Extra object at runtime? | Real type at runtime | |
|---|---|---|---|
typedef | No, just another name | No | int |
extension type | Yes | No | int |
class | Yes | Yes | Ticket |
A typedef is only a nickname, so it blocks no mix-ups. We will study typedefs in the next lecture. A class is a real object with fields and a runtime identity. An extension type sits in between: the safety of a new type at the cost of a typedef.
9. A Real-World Example
extension type AccountId(int value) {}
extension type Rupees(int amount) {
Rupees add(Rupees other) => Rupees(amount + other.amount);
String get label => 'Rs $amount';
}
void deposit(AccountId account, Rupees money) {
print('Account #${account.value} receives ${money.label}');
}
void main() {
var sara = AccountId(501);
var salary = Rupees(40000);
var bonus = Rupees(5000);
deposit(sara, salary.add(bonus));
// deposit(salary, sara); // ✗ arguments swapped: compile-time error
}Account #501 receives Rs 45000AccountId and Rupees are both plain ints underneath, but swapping the two arguments no
longer compiles. Rupees exposes only what money needs: add and label.
Code Examples
Example 1 — A First Extension Type
extension type ProductId(int value) {}
void main() {
var id = ProductId(305);
print('Product #${id.value}');
}Product #305Example 2 — A String Representation
extension type Email(String address) {
String get domain => address.split('@').last;
}
void main() {
var email = Email('sara@school.pk');
print(email.domain);
}school.pkExample 3 — A Method
extension type Minutes(int value) {
String format() => '${value ~/ 60}h ${value % 60}m';
}
void main() {
print(Minutes(135).format());
}2h 15mExample 4 — A Named Constructor
extension type Minutes(int value) {
Minutes.fromHours(int hours) : value = hours * 60;
}
void main() {
print(Minutes.fromHours(3).value);
}180Example 5 — implements int
extension type Age(int years) implements int {}
void main() {
var zayan = Age(6);
print(zayan + 1);
print(zayan > 5);
}7
trueExample 6 — Seeing Through the Wrapper
extension type Email(String address) {}
void main() {
var email = Email('ali@mail.com');
print(email.runtimeType);
print(email is String);
}String
trueExample 7 — Student Marks
extension type Marks._(int value) {
factory Marks(int value) => Marks._(value.clamp(0, 100));
bool get passed => value >= 50;
}
void main() {
var ali = Marks(105);
var zayan = Marks(42);
print('Ali: ${ali.value}, passed: ${ali.passed}');
print('Zayan: ${zayan.value}, passed: ${zayan.passed}');
}Ali: 100, passed: true
Zayan: 42, passed: falseHow it works
- The factory clamps
105to100before wrapping it. passedis a getter built onvalue.- Nothing outside the type can create a
Marksabove 100.
Example 8 — Shopping Total
extension type Price(int rupees) {
Price times(int quantity) => Price(rupees * quantity);
}
void main() {
var notebook = Price(500);
var total = notebook.times(3);
print('Total: Rs ${total.rupees}');
}Total: Rs 1500How it works
Pricehidesint's operators, so you can't add a price to a quantity by mistake.timesgives a newPrice, so the result keeps its meaning.
Example 9 — Bank Balance
extension type AccountId(int value) {}
void printBalance(AccountId id, int balance) {
print('Account #${id.value}: $balance');
}
void main() {
var ali = AccountId(1001);
printBalance(ali, 2500);
// printBalance(2500, ali); // ✗ swapped: compile-time error
}Account #1001: 2500How it works
idmust be anAccountId, so a bare2500is rejected.- Swapping the arguments is caught before the program runs.
Example 10 — Temperature
extension type Celsius(double value) {
double get fahrenheit => value * 9 / 5 + 32;
}
void main() {
var readings = [Celsius(20), Celsius(25), Celsius(30)];
for (var reading in readings) {
print('${reading.value} C = ${reading.fahrenheit} F');
}
}20.0 C = 68.0 F
25.0 C = 77.0 F
30.0 C = 86.0 FHow it works
- The list holds plain doubles at runtime, with no extra objects.
- While checking, each element is a
Celsius, sofahrenheitis available.
Common Beginner Mistakes
1. Declaring an instance field
extension type Celsius(double value) {
// int count = 0; // ✗
}Error: Extension types can't declare instance fields
Try removing the field declaration or making it a static fieldThe representation is the only data. Use a getter, or a class if you need more fields.
2. Overriding toString()
extension type Celsius(double value) {
// String toString() => '$value C'; // ✗
}Error: This extension member conflicts with Object member 'toString'.At runtime the value is a plain double, so its own toString() is used. Write a method
such as describe() instead.
3. Expecting the representation's members
extension type UserId(int id) {}
void main() {
var ali = UserId(8);
// print(ali.isEven); // ✗
print(ali.id.isEven);
}Error: The getter 'isEven' isn't defined for the type 'UserId'.Go through the field (ali.id.isEven), or add implements int if the whole API belongs.
4. Trusting is to tell IDs apart
userId is OrderId is true at runtime when both wrap an int. Make the types match
while checking; never rely on is or as for this.
5. Assigning to the representation
extension type UserId(int id) {}
void main() {
var ali = UserId(7);
// ali.id = 8; // ✗
ali = UserId(8);
print(ali.id);
}Error: The setter 'id' isn't defined for the type 'UserId'.The representation is final. Create a new value instead.
Best Practices
- Use an extension type when two values share a representation but must never be mixed.
- Hide the representation by default; add
implementsonly when its whole API makes sense. - Use a private primary constructor plus a factory to clean values on the way in.
- Don't use
isorasto protect an extension type. They see straight through it. - Use a class when you need extra fields, a runtime identity, or a custom
toString().
Mini Challenge
Create extension types Kilograms and Pounds, both wrapping a double. Give Pounds a
getter toKilograms that returns a Kilograms (1 pound = 0.5 kg for this exercise).
Convert 10 pounds and print the result.
10.0 lb = 5.0 kgTry it yourself before opening the solution.
Show solution
extension type Kilograms(double value) {}
extension type Pounds(double value) {
Kilograms get toKilograms => Kilograms(value * 0.5);
}
void main() {
var bag = Pounds(10);
var kg = bag.toKilograms;
print('${bag.value} lb = ${kg.value} kg');
}10.0 lb = 5.0 kgHow it works
Pounds(10)turns the literal10into thedouble10.0.- The getter returns a
Kilograms, so a pounds value can't be passed where kilograms are expected.
Practice Questions
Work through these in order. They run from beginner to application to challenge.
Beginner
- Declare
extension type BookId(int value) {}, createBookId(12)and printBook #12. - Declare
Usernamewrapping aString, with a gettershoutreturning the name in upper case. Print it for'zayan'. - What does
print(BookId(12).runtimeType);print, and why?
Application
- Give
Minutes(wrapping anint) a named constructorMinutes.fromHours(int hours). Print the value for 2 hours. - Declare
Level(int value) implements intand printLevel(4) * 2. - Declare
Percent._(int value)with a factory that clamps to 0–100. Print the value for150and for70. - Declare
StudentIdandCourseId, both wrappingint, and a functionenroll(StudentId s, CourseId c)that printsStudent 3 joins course 9. Call it correctly, and add the swapped call as a comment marked// ✗.
Challenge
-
Predict the output of this program, then explain the last line:
extension type UserId(int id) {} extension type OrderId(int id) {} void main() { Object? value = UserId(5); print(value is int); print(value is OrderId); } -
Create
Rupees(int amount)with a methodplus(Rupees other)returningRupees. Add Ali's300and Sara's450, and printTotal: Rs 750. -
Sara stores a phone number as a
String. Should it be atypedef, an extension type or a class if she wants it never to be mixed up with other strings, without extra objects? Write it with a getterareaCodereturning the first 4 characters, and print it for'0300-1234567'.
Practice Answers
Question 1 — Solution
extension type BookId(int value) {}
void main() {
var book = BookId(12);
print('Book #${book.value}');
}Book #12Question 2 — Solution
extension type Username(String name) {
String get shout => name.toUpperCase();
}
void main() {
print(Username('zayan').shout);
}ZAYANQuestion 3 — Solution
extension type BookId(int value) {}
void main() {
print(BookId(12).runtimeType);
}intThe extension type only exists while Dart checks the code. At runtime the value is an int.
Question 4 — Solution
extension type Minutes(int value) {
Minutes.fromHours(int hours) : value = hours * 60;
}
void main() {
print(Minutes.fromHours(2).value);
}120Question 5 — Solution
extension type Level(int value) implements int {}
void main() {
print(Level(4) * 2);
}8Question 6 — Solution
extension type Percent._(int value) {
factory Percent(int value) => Percent._(value.clamp(0, 100));
}
void main() {
print(Percent(150).value);
print(Percent(70).value);
}100
70Question 7 — Solution
extension type StudentId(int value) {}
extension type CourseId(int value) {}
void enroll(StudentId s, CourseId c) {
print('Student ${s.value} joins course ${c.value}');
}
void main() {
enroll(StudentId(3), CourseId(9));
// enroll(CourseId(9), StudentId(3)); // ✗
}Student 3 joins course 9Question 8 — Solution
true
trueAt runtime a UserId is just an int, and is OrderId only checks for an int. So the
check can't tell the two IDs apart.
Question 9 — Solution
extension type Rupees(int amount) {
Rupees plus(Rupees other) => Rupees(amount + other.amount);
}
void main() {
var total = Rupees(300).plus(Rupees(450));
print('Total: Rs ${total.amount}');
}Total: Rs 750Question 10 — Solution
An extension type: a typedef wouldn't block mix-ups, and a class would build an extra object.
extension type PhoneNumber(String digits) {
String get areaCode => digits.substring(0, 4);
}
void main() {
var phone = PhoneNumber('0300-1234567');
print(phone.areaCode);
}0300Quick Revision
- What is the representation of
extension type UserId(int id) {}? - What does "zero-cost" mean for an extension type?
- Why does passing an
OrderIdto aUserIdparameter fail? - Can an extension type declare instance fields?
- What does
implements intchange? - What does
UserId(7).runtimeTypeprint? - When is a class a better choice than an extension type?
Key Takeaways
- An extension type gives an existing representation a new static type.
extension type UserId(int id) {}wraps anint;idis final.- Different extension types, and the bare representation, can't be mixed. Mix-ups become compile-time errors.
- Members are getters, methods, constructors and statics, but no instance fields.
- The representation's API is hidden unless you write
implements. - The wrapper is erased at runtime:
print,runtimeType,isandasall see the plain value. - Typedef = new name; extension type = new type, no runtime cost; class = new type and a real object.
What's Next?
Lecture 71 — Typedefs. We met the typedef here as "just a nickname". That sounds weak next to extension types, but nicknames are very useful when a type is long and hard to read, especially function types. Next we will learn how typedefs make such types short and clear.