Watch the video (10 min)
On this page21 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. The Problem: Two Kinds of int
  5. 2. Declaring an Extension Type
  6. 3. Mix-ups Become Errors
  7. 4. Adding Members
  8. 5. Constructors
  9. 6. Hiding vs Exposing the Representation
  10. 7. The Wrapper Disappears at Runtime
  11. 8. Extension Type vs Class vs Typedef
  12. 9. A Real-World Example
  13. Code Examples
  14. Common Beginner Mistakes
  15. Best Practices
  16. Mini Challenge
  17. Practice Questions
  18. Practice Answers
  19. Quick Revision
  20. Key Takeaways
  21. What's Next?

Lecture 70 — Extension Types

Category11 — Enums, Extensions & Typedefs
Lecture070 of 140
Estimated video length10 min
TargetDart 3.12.2
LevelIntermediate

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 is and as, and runtimeType (Lecture 010)
  • classes, getters, and named, constant and factory constructors (Lecture 055)
  • implements from 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.

dart
void cancelOrder(int orderId) {
  print('Cancelling order #$orderId');
}

void main() {
  var userId = 7;
  var orderId = 1042;
  cancelOrder(userId);
}
output
Cancelling order #7

That'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 is and as see 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:

dart
extension type UserId(int id) {}

void main() {
  var ali = UserId(7);
  print(ali.id);
}
output
7

UserId(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:

dart
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);
}
output
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:

dart
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);
}
output
25.0°C
77.0
false

An 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:

dart
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}');
}
output
1 true 42

A 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:

dart
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);
}
output
100
0
88

Rejecting 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:

dart
extension type UserId(int id) {}

void main() {
  var ali = UserId(8);
  print(ali.isEven);
}
output
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:

dart
extension type Score(int value) implements int {}

void main() {
  var score = Score(40);
  print(score + 5);
  print(score.isEven);
  int plain = score;
  print(plain);
}
output
45
true
40

Now 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:

dart
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);
}
output
7
int
true
true
7

This 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

dart
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);
}
output
5
true
false
New type while checking?Extra object at runtime?Real type at runtime
typedefNo, just another nameNoint
extension typeYesNoint
classYesYesTicket

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

dart
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
}
output
Account #501 receives Rs 45000

AccountId 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

dart
extension type ProductId(int value) {}

void main() {
  var id = ProductId(305);
  print('Product #${id.value}');
}
output
Product #305

Example 2 — A String Representation

dart
extension type Email(String address) {
  String get domain => address.split('@').last;
}

void main() {
  var email = Email('sara@school.pk');
  print(email.domain);
}
output
school.pk

Example 3 — A Method

dart
extension type Minutes(int value) {
  String format() => '${value ~/ 60}h ${value % 60}m';
}

void main() {
  print(Minutes(135).format());
}
output
2h 15m

Example 4 — A Named Constructor

dart
extension type Minutes(int value) {
  Minutes.fromHours(int hours) : value = hours * 60;
}

void main() {
  print(Minutes.fromHours(3).value);
}
output
180

Example 5 — implements int

dart
extension type Age(int years) implements int {}

void main() {
  var zayan = Age(6);
  print(zayan + 1);
  print(zayan > 5);
}
output
7
true

Example 6 — Seeing Through the Wrapper

dart
extension type Email(String address) {}

void main() {
  var email = Email('ali@mail.com');
  print(email.runtimeType);
  print(email is String);
}
output
String
true

Example 7 — Student Marks

dart
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}');
}
output
Ali: 100, passed: true
Zayan: 42, passed: false

How it works

  1. The factory clamps 105 to 100 before wrapping it.
  2. passed is a getter built on value.
  3. Nothing outside the type can create a Marks above 100.

Example 8 — Shopping Total

dart
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}');
}
output
Total: Rs 1500

How it works

  1. Price hides int's operators, so you can't add a price to a quantity by mistake.
  2. times gives a new Price, so the result keeps its meaning.

Example 9 — Bank Balance

dart
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
}
output
Account #1001: 2500

How it works

  1. id must be an AccountId, so a bare 2500 is rejected.
  2. Swapping the arguments is caught before the program runs.

Example 10 — Temperature

dart
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');
  }
}
output
20.0 C = 68.0 F
25.0 C = 77.0 F
30.0 C = 86.0 F

How it works

  1. The list holds plain doubles at runtime, with no extra objects.
  2. While checking, each element is a Celsius, so fahrenheit is available.

Common Beginner Mistakes

1. Declaring an instance field

dart
extension type Celsius(double value) {
  // int count = 0; // ✗
}
output
Error: Extension types can't declare instance fields
Try removing the field declaration or making it a static field

The representation is the only data. Use a getter, or a class if you need more fields.

2. Overriding toString()

dart
extension type Celsius(double value) {
  // String toString() => '$value C'; // ✗
}
output
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

dart
extension type UserId(int id) {}

void main() {
  var ali = UserId(8);
  // print(ali.isEven); // ✗
  print(ali.id.isEven);
}
output
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

dart
extension type UserId(int id) {}

void main() {
  var ali = UserId(7);
  // ali.id = 8; // ✗
  ali = UserId(8);
  print(ali.id);
}
output
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 implements only when its whole API makes sense.
  • Use a private primary constructor plus a factory to clean values on the way in.
  • Don't use is or as to 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.

output
10.0 lb = 5.0 kg

Try it yourself before opening the solution.

Show solution
dart
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');
}
output
10.0 lb = 5.0 kg

How it works

  1. Pounds(10) turns the literal 10 into the double 10.0.
  2. 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

  1. Declare extension type BookId(int value) {}, create BookId(12) and print Book #12.
  2. Declare Username wrapping a String, with a getter shout returning the name in upper case. Print it for 'zayan'.
  3. What does print(BookId(12).runtimeType); print, and why?

Application

  1. Give Minutes (wrapping an int) a named constructor Minutes.fromHours(int hours). Print the value for 2 hours.
  2. Declare Level(int value) implements int and print Level(4) * 2.
  3. Declare Percent._(int value) with a factory that clamps to 0–100. Print the value for 150 and for 70.
  4. Declare StudentId and CourseId, both wrapping int, and a function enroll(StudentId s, CourseId c) that prints Student 3 joins course 9. Call it correctly, and add the swapped call as a comment marked // ✗.

Challenge

  1. Predict the output of this program, then explain the last line:

    dart
    extension type UserId(int id) {}
    
    extension type OrderId(int id) {}
    
    void main() {
      Object? value = UserId(5);
      print(value is int);
      print(value is OrderId);
    }
  2. Create Rupees(int amount) with a method plus(Rupees other) returning Rupees. Add Ali's 300 and Sara's 450, and print Total: Rs 750.

  3. Sara stores a phone number as a String. Should it be a typedef, 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 getter areaCode returning the first 4 characters, and print it for '0300-1234567'.


Practice Answers

Question 1 — Solution

dart
extension type BookId(int value) {}

void main() {
  var book = BookId(12);
  print('Book #${book.value}');
}
output
Book #12

Question 2 — Solution

dart
extension type Username(String name) {
  String get shout => name.toUpperCase();
}

void main() {
  print(Username('zayan').shout);
}
output
ZAYAN

Question 3 — Solution

dart
extension type BookId(int value) {}

void main() {
  print(BookId(12).runtimeType);
}
output
int

The extension type only exists while Dart checks the code. At runtime the value is an int.

Question 4 — Solution

dart
extension type Minutes(int value) {
  Minutes.fromHours(int hours) : value = hours * 60;
}

void main() {
  print(Minutes.fromHours(2).value);
}
output
120

Question 5 — Solution

dart
extension type Level(int value) implements int {}

void main() {
  print(Level(4) * 2);
}
output
8

Question 6 — Solution

dart
extension type Percent._(int value) {
  factory Percent(int value) => Percent._(value.clamp(0, 100));
}

void main() {
  print(Percent(150).value);
  print(Percent(70).value);
}
output
100
70

Question 7 — Solution

dart
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)); // ✗
}
output
Student 3 joins course 9

Question 8 — Solution

output
true
true

At 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

dart
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}');
}
output
Total: Rs 750

Question 10 — Solution

An extension type: a typedef wouldn't block mix-ups, and a class would build an extra object.

dart
extension type PhoneNumber(String digits) {
  String get areaCode => digits.substring(0, 4);
}

void main() {
  var phone = PhoneNumber('0300-1234567');
  print(phone.areaCode);
}
output
0300

Quick Revision

  1. What is the representation of extension type UserId(int id) {}?
  2. What does "zero-cost" mean for an extension type?
  3. Why does passing an OrderId to a UserId parameter fail?
  4. Can an extension type declare instance fields?
  5. What does implements int change?
  6. What does UserId(7).runtimeType print?
  7. 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 an int; id is 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, is and as all 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.


Video lecture

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