On this page23 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. The Problem: An Open Family
- 2. Declaring a Sealed Class
- 3. A Sealed Class Is Implicitly Abstract
- 4. Subtypes Must Live in the Same File
- 5. Exhaustive switch Without _
- 6. A Missing Subtype Is an Error
- 7. Object Patterns in the Cases
- 8. Modelling Outcomes: PaymentResult
- 9. Result<T> Revisited as a Sealed Type
- 10. A New Subtype: The Compiler Finds Every switch
- 11. 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 82 — Sealed Classes
| Category | 13 — Patterns & Modern Dart |
| Lecture | 082 of 140 |
| Estimated video length | 12 min |
| Target | Dart 3.12.2 |
| Level | Intermediate |
A switch over subclasses always needed
_, because anyone could add another subclass.sealedcloses the family, so Dart knows every subtype and tells you when a case is missing.
Contents
Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways
Every error and warning in this lecture is real. Errors come from
dart run, warnings fromdart analyze, on Dart 3.12.2.
Prerequisites
Students should already understand, from Lectures 053–081:
- classes, constructors,
extends, and files connected withimport(Lectures 053–060) - abstract classes and polymorphism (Lectures 062 and 065)
- enums and exhaustive switches on them (Lecture 067)
- generic classes, including the
Result<T>holder (Lecture 073) - object patterns and pattern
switchwith guards (Lectures 080–081)
Connection With Previous Lectures
In Lecture 081, Dart checked that a switch on bool, an enum, or a record covered every case.
A switch over an ordinary class's subclasses still needed _ — and that _ hides mistakes: a
new subclass quietly falls into it. We need a class family Dart knows is complete.
Learning Objectives
By the end of this lecture you will be able to:
- Declare a
sealed classand its subclasses - Explain why a sealed class is implicitly abstract
- Recognize that subtypes must be declared in the same file (library)
- Write exhaustive switches over subtypes with no
_arm - Read the error Dart gives when a subtype is missing
- Use object patterns to read each subtype's fields in its case
- Model results such as
SuccessandFailure, including a sealedResult<T> - Let the compiler find every switch to update when a new subtype is added
1. The Problem: An Open Family
Here is an abstract Shape with two subclasses, and a switch covering both:
abstract class Shape {}
class Circle extends Shape {}
class Square extends Shape {}
void main() {
Shape shape = Circle();
// var name = switch (shape) {
// Circle() => 'circle',
// Square() => 'square',
// }; // ✗
}Error: The type 'Shape' is not exhaustively matched by the switch cases since it doesn't match 'Shape()'.
Try adding a wildcard pattern or cases that match 'Shape()'.Dart is right: any other file could add class Triangle extends Shape, and this switch would
have nothing for it.
2. Declaring a Sealed Class
Write sealed in front of class. The subclasses are written exactly as before:
Analogy — a sealed class is a guest list printed on the invitation. Nobody can join later, so the host knows every guest in advance.
sealed class Shape {}
class Circle extends Shape {
final double radius;
Circle(this.radius);
}
class Square extends Shape {
final double side;
Square(this.side);
}
void main() {
Shape shape = Square(4);
print(shape is Square);
}trueA sealed class can hold fields, constructors, and methods, like any abstract class (Lecture 062).
3. A Sealed Class Is Implicitly Abstract
You never write abstract sealed: a sealed class is abstract automatically, so you cannot
create one directly:
sealed class Shape {}
class Circle extends Shape {}
void main() {
// var oops = Shape(); // ✗
var shape = Circle();
print(shape);
}Error: The class 'Shape' is abstract and can't be instantiated.Every real object must be one of the subtypes.
4. Subtypes Must Live in the Same File
Every class that extends or implements a sealed class must be in the same library — the
same .dart file (Lecture 057). Put Shape in shapes.dart:
shapes.dart
sealed class Shape {}
class Circle extends Shape {}Now try to add subtypes from main.dart:
main.dart
import 'shapes.dart';
// class Triangle extends Shape {} // ✗
// class Hexagon implements Shape {} // ✗
void main() {
print(Circle());
}main.dart:3:24: Error: The class 'Shape' can't be extended, implemented, or mixed in outside of its library because it's a sealed class.
main.dart:4:26: Error: The class 'Shape' can't be extended, implemented, or mixed in outside of its library because it's a sealed class.main.dart can still use Shape and Circle; it just cannot add to the family. So Dart
sees every subtype by reading one file. We will study libraries in Lectures 101–102.
5. Exhaustive switch Without _
The switch that failed in section 1 now compiles, with no fallback arm:
sealed class Shape {}
class Circle extends Shape {}
class Square extends Shape {}
String name(Shape shape) => switch (shape) {
Circle() => 'circle',
Square() => 'square',
};
void main() {
print(name(Circle()));
print(name(Square()));
}circle
squareCircle() is an object pattern with no fields (Lecture 080): it only checks the type. Switch
statements on a sealed type must be exhaustive too, as on an enum.
6. A Missing Subtype Is an Error
Add Triangle to the family, but forget it in the switch:
sealed class Shape {}
class Circle extends Shape {}
class Square extends Shape {}
class Triangle extends Shape {}
void main() {
Shape shape = Triangle();
// switch (shape) {
// case Circle():
// print('Round');
// case Square():
// print('Four corners');
// } // ✗
}Error: The type 'Shape' is not exhaustively matched by the switch cases since it doesn't match 'Triangle()'.
Try adding a default case or cases that match 'Triangle()'.Dart names the subtype you forgot, as it does for enums.
7. Object Patterns in the Cases
An object pattern checks the type and reads that subtype's own fields:
sealed class Shape {}
class Circle extends Shape {
final double radius;
Circle(this.radius);
}
class Square extends Shape {
final double side;
Square(this.side);
}
double area(Shape shape) => switch (shape) {
Circle(:var radius) => 3.14 * radius * radius,
Square(:var side) => side * side,
};
void main() {
print(area(Circle(2)));
print(area(Square(3)));
}12.56
9.0radius is a double inside its arm — no is check. Unlike an enum value, each Circle can
hold its own radius.
8. Modelling Outcomes: PaymentResult
Sealed classes shine when something can end in a few different ways, each with its own details:
sealed class PaymentResult {}
class Success extends PaymentResult {
final String receipt;
Success(this.receipt);
}
class Failure extends PaymentResult {
final String reason;
Failure(this.reason);
}
PaymentResult pay(int balance, int amount) => amount <= balance
? Success('RCPT-${balance - amount}')
: Failure('Not enough money');
void main() {
for (var amount in [300, 900]) {
var message = switch (pay(500, amount)) {
Success(:var receipt) => 'Paid. Receipt $receipt',
Failure(:var reason) => 'Declined: $reason',
};
print(message);
}
}Paid. Receipt RCPT-200
Declined: Not enough moneyA Success has no reason and a Failure has no receipt, so no field is ever null.
9. Result<T> Revisited as a Sealed Type
Lecture 073's Result<T> had two nullable fields and an isOk flag. As a sealed generic
family, each case holds only what it needs:
sealed class Result<T> {}
class Ok<T> extends Result<T> {
final T value;
Ok(this.value);
}
class Fail<T> extends Result<T> {
final String error;
Fail(this.error);
}
Result<int> parseMarks(String text) {
var marks = int.tryParse(text);
return marks == null ? Fail('"$text" is not a number') : Ok(marks);
}
void main() {
for (var input in ['85', 'abc']) {
print(switch (parseMarks(input)) {
Ok(:var value) => 'Marks: $value',
Fail(:var error) => 'Error: $error',
});
}
}Marks: 85
Error: "abc" is not a numbervalue is a non-nullable int, reachable only in the Ok arm. Exceptions start in
Lecture 083.
10. A New Subtype: The Compiler Finds Every switch
The biggest payoff comes when the family grows. Add a Triangle to a family with two
switches:
sealed class Shape {}
class Circle extends Shape {}
class Square extends Shape {}
class Triangle extends Shape {} // new
// String name(Shape s) => switch (s) {
// Circle() => 'circle',
// Square() => 'square',
// }; // ✗
void describe(Shape s) {
// switch (s) {
// case Circle():
// print('c');
// case Square():
// print('s');
// } // ✗
}
void main() {
describe(Triangle());
}main.dart:9:33: Error: The type 'Shape' is not exhaustively matched by the switch cases since it doesn't match 'Triangle()'.
Try adding a wildcard pattern or cases that match 'Triangle()'.
main.dart:15:11: Error: The type 'Shape' is not exhaustively matched by the switch cases since it doesn't match 'Triangle()'.
Try adding a default case or cases that match 'Triangle()'.The error list is a to-do list of every switch to update. With _, both would have
compiled and silently mishandled triangles.
11. A Real-World Example
Sara's bank app records three kinds of transaction, and Dart checks all three are handled:
sealed class Transaction {
final int amount;
Transaction(this.amount);
}
class Deposit extends Transaction {
Deposit(super.amount);
}
class Withdrawal extends Transaction {
Withdrawal(super.amount);
}
class Transfer extends Transaction {
final String to;
Transfer(this.to, super.amount);
}
void main() {
var history = [Deposit(5000), Withdrawal(1200), Transfer('Ali', 800)];
var balance = 0;
for (var t in history) {
balance += switch (t) {
Deposit(:var amount) => amount,
Withdrawal(:var amount) || Transfer(:var amount) => -amount,
};
var line = switch (t) {
Deposit(:var amount) => '+ $amount deposit',
Withdrawal(:var amount) when amount > 1000 =>
'- $amount large withdrawal',
Withdrawal(:var amount) => '- $amount withdrawal',
Transfer(:var to, :var amount) => '- $amount to $to',
};
print('$line (balance $balance)');
}
}+ 5000 deposit (balance 5000)
- 1200 large withdrawal (balance 3800)
- 800 to Ali (balance 3000)amount lives on the parent and Transfer adds to. The guarded Withdrawal arm is backed by
an unguarded one (Lecture 081).
Code Examples
Example 1 — Two Subtypes
sealed class Pet {}
class Dog extends Pet {}
class Cat extends Pet {}
void main() {
Pet pet = Cat();
var sound = switch (pet) {
Dog() => 'Woof',
Cat() => 'Meow',
};
print(sound);
}MeowExample 2 — Reading Fields
sealed class Shape {}
class Rectangle extends Shape {
final int width, height;
Rectangle(this.width, this.height);
}
class Square extends Shape {
final int side;
Square(this.side);
}
void main() {
Shape shape = Rectangle(3, 5);
var area = switch (shape) {
Rectangle(:var width, :var height) => width * height,
Square(:var side) => side * side,
};
print('Area: $area');
}Area: 15Example 3 — A switch Statement
sealed class Vehicle {}
class Car extends Vehicle {}
class Bike extends Vehicle {}
void main() {
Vehicle v = Bike();
switch (v) {
case Car():
print('Four wheels');
case Bike():
print('Two wheels');
}
}Two wheelsExample 4 — A Shared Field on the Parent
sealed class Message {
final String from;
Message(this.from);
}
class TextMessage extends Message {
final String body;
TextMessage(super.from, this.body);
}
class Photo extends Message {
Photo(super.from);
}
void main() {
Message m = TextMessage('Zayan', 'See you at 5');
print('From: ${m.from}');
print(switch (m) {
TextMessage(:var body) => 'Says: $body',
Photo() => 'Sent a photo',
});
}From: Zayan
Says: See you at 5Example 5 — Guard Plus Fallback
sealed class Delivery {}
class Packed extends Delivery {}
class Shipped extends Delivery {
final int days;
Shipped(this.days);
}
class Delivered extends Delivery {}
void main() {
Delivery d = Shipped(1);
print(switch (d) {
Packed() => 'Leaving soon',
Shipped(days: < 2) => 'Arrives tomorrow',
Shipped(:var days) => 'Arrives in $days days',
Delivered() => 'At your door',
});
}Arrives tomorrowExample 6 — A Generic Sealed Family
sealed class Lookup<T> {}
class Found<T> extends Lookup<T> {
final T value;
Found(this.value);
}
class Missing<T> extends Lookup<T> {}
Lookup<int> findAge(String name) => name == 'Sara' ? Found(21) : Missing();
void main() {
for (var name in ['Sara', 'Omar']) {
print(switch (findAge(name)) {
Found(:var value) => '$name is $value',
Missing() => 'No age for $name',
});
}
}Sara is 21
No age for OmarExample 7 — Student Marks
sealed class ExamResult {}
class Sat extends ExamResult {
final int marks;
Sat(this.marks);
}
class Absent extends ExamResult {}
void main() {
var results = {'Ali': Sat(82), 'Sara': Sat(45), 'Zayan': Absent()};
results.forEach((name, result) {
var verdict = switch (result) {
Sat(marks: >= 50) => 'Pass',
Sat() => 'Fail',
Absent() => 'Absent',
};
print('$name: $verdict');
});
}Ali: Pass
Sara: Fail
Zayan: AbsentExample 8 — Shopping Total
sealed class Discount {}
class Percent extends Discount {
final int rate;
Percent(this.rate);
}
class Flat extends Discount {
final int amount;
Flat(this.amount);
}
class NoDiscount extends Discount {}
int finalTotal(int total, Discount d) => switch (d) {
Percent(:var rate) => total - total * rate ~/ 100,
Flat(:var amount) => total - amount,
NoDiscount() => total,
};
void main() {
print(finalTotal(4000, Percent(10)));
print(finalTotal(4000, Flat(500)));
print(finalTotal(4000, NoDiscount()));
}3600
3500
4000Example 9 — Bank Withdrawal
sealed class Withdraw {}
class Approved extends Withdraw {
final double newBalance;
Approved(this.newBalance);
}
class Declined extends Withdraw {
final double shortBy;
Declined(this.shortBy);
}
Withdraw withdraw(double balance, double amount) =>
amount <= balance ? Approved(balance - amount) : Declined(amount - balance);
void main() {
for (var amount in [700.0, 1500.0]) {
print(switch (withdraw(1000, amount)) {
Approved(:var newBalance) => 'Done. Balance: $newBalance',
Declined(:var shortBy) => 'Declined. Short by $shortBy',
});
}
}Done. Balance: 300.0
Declined. Short by 500.0Example 10 — Temperature Readings
sealed class Reading {
final double value;
Reading(this.value);
}
class Celsius extends Reading {
Celsius(super.value);
}
class Fahrenheit extends Reading {
Fahrenheit(super.value);
}
double toCelsius(Reading r) => switch (r) {
Celsius(:var value) => value,
Fahrenheit(:var value) => (value - 32) * 5 / 9,
};
void main() {
print(toCelsius(Celsius(25)));
print(toCelsius(Fahrenheit(212)));
}25.0
100.0Common Beginner Mistakes
1. Extending a sealed class from another file — gives The class 'Shape' can't be extended, implemented, or mixed in outside of its library because it's a sealed class.
(section 4). Move the subclass into the sealed class's file.
2. Creating the sealed parent — Shape() gives The class 'Shape' is abstract and can't be instantiated. Create a subtype instead.
3. Forgetting the parentheses in a type case
sealed class Shape {}
class Circle extends Shape {}
class Square extends Shape {}
void main() {
Shape s = Circle();
// var n = switch (s) {
// Circle => 'circle',
// Square => 'square',
// }; // ✗
}Error: The type 'Shape' is not exhaustively matched by the switch cases since it doesn't match 'Circle()'.Without (), Circle is a constant of type Type, not a type test (dart analyze warns it
can never match). Write Circle().
4. Covering only part of a subtype
sealed class Shape {}
class Circle extends Shape {
final double radius;
Circle(this.radius);
}
class Square extends Shape {}
void main() {
Shape s = Circle(3);
// var size = switch (s) {
// Circle(radius: > 10) => 'big circle',
// Square() => 'square',
// }; // ✗
}Error: The type 'Shape' is not exhaustively matched by the switch cases since it doesn't match 'Circle(radius: double())'.
Try adding a wildcard pattern or cases that match 'Circle()'.Add a plain Circle() arm after the specific one.
5. Adding _ "just in case" — dart analyze warns This case is covered by the previous cases., and the _ would silently swallow future subtypes.
Best Practices
- Use
sealedwhen a type has a fixed set of kinds that carry different data; use an enum when the kinds carry none. - Keep the sealed parent and all its subtypes together in one file.
- Don't add
_to a switch over a sealed type — let Dart point you to every switch when the family grows. - Prefer a sealed
Success/Failurefamily over nullable fields plus anisOkflag.
Mini Challenge
A cinema sells three tickets: Adult (Rs 800), Child with an age (free under 3,
otherwise Rs 400), and Senior (Rs 500). Make Ticket sealed and print the total for
Adult(), Child(2), Child(9), Senior() — with no _ arm.
Total: Rs 1700Try it yourself before opening the solution.
Show solution
sealed class Ticket {}
class Adult extends Ticket {}
class Child extends Ticket {
final int age;
Child(this.age);
}
class Senior extends Ticket {}
void main() {
var tickets = [Adult(), Child(2), Child(9), Senior()];
var total = 0;
for (var t in tickets) {
total += switch (t) {
Adult() => 800,
Child(age: < 3) => 0,
Child() => 400,
Senior() => 500,
};
}
print('Total: Rs $total');
}Total: Rs 1700Practice Questions
Work through these in order — they run from beginner to application to challenge.
Beginner
- Declare
sealed class Drinkwith subclassesTeaandCoffee. Store aCoffee()in aDrinkvariable and printCoffeeorTeawith a switch expression. - What happens if you write
var d = Drink();? Why? - Can
class Juice extends Drink {}be written in a different file fromDrink? Explain.
Application
- Make
sealed class ShapewithCircle(radius)andRectangle(width, height)(bothdouble). Writedouble perimeter(Shape s)using object patterns and printperimeter(Rectangle(2, 3)). Use3.14for π. - Make
sealed class LoginResultwithLoggedIn(name)andWrongPassword(triesLeft). PrintWelcome, SaraforLoggedIn('Sara')and2 tries leftforWrongPassword(2). - Using
sealed class LightwithRed,Yellow,Green, write a switch statement that printsStop,Slow down, orGoforYellow(). - Add a fourth subclass
Flashingto question 6 without touching the switch. What does Dart report, and how do you fix it?
Challenge
-
Using question 4's
Shape, why does this switch fail to compile? Fix it so it printssmall circleforCircle(2):String size(Shape s) => switch (s) { Circle(radius: >= 5) => 'big circle', Rectangle() => 'rectangle', }; -
Write
sealed class Result<T>withOk<T>(value)andFail<T>(error), andResult<int> safeDivide(int a, int b)that fails withCannot divide by zero. Print the results ofsafeDivide(10, 2)andsafeDivide(1, 0)asResult: 5andError: Cannot divide by zero. -
Zayan's game has
sealed class MovewithStep(steps),Jump(height), andRest(). Starting atposition = 0, aStepadds its steps, aJumpaddsheight * 2, and aRestadds nothing. Process[Step(3), Jump(2), Rest(), Step(1)]and printPosition: 8.
Practice Answers
Question 1 — Solution
sealed class Drink {}
class Tea extends Drink {}
class Coffee extends Drink {}
void main() {
Drink drink = Coffee();
print(switch (drink) {
Tea() => 'Tea',
Coffee() => 'Coffee',
});
}CoffeeQuestion 2 — Solution
It does not compile: The class 'Drink' is abstract and can't be instantiated. A sealed class
is implicitly abstract.
Question 3 — Solution
No. Subtypes must be in the same library (file); elsewhere Dart says Drink can't be extended
outside of its library because it's a sealed class.
Question 4 — Solution
sealed class Shape {}
class Circle extends Shape {
final double radius;
Circle(this.radius);
}
class Rectangle extends Shape {
final double width, height;
Rectangle(this.width, this.height);
}
double perimeter(Shape s) => switch (s) {
Circle(:var radius) => 2 * 3.14 * radius,
Rectangle(:var width, :var height) => 2 * (width + height),
};
void main() {
print(perimeter(Rectangle(2, 3)));
}10.0Question 5 — Solution
sealed class LoginResult {}
class LoggedIn extends LoginResult {
final String name;
LoggedIn(this.name);
}
class WrongPassword extends LoginResult {
final int triesLeft;
WrongPassword(this.triesLeft);
}
String message(LoginResult r) => switch (r) {
LoggedIn(:var name) => 'Welcome, $name',
WrongPassword(:var triesLeft) => '$triesLeft tries left',
};
void main() {
print(message(LoggedIn('Sara')));
print(message(WrongPassword(2)));
}Welcome, Sara
2 tries leftQuestion 6 — Solution
sealed class Light {}
class Red extends Light {}
class Yellow extends Light {}
class Green extends Light {}
void main() {
Light light = Yellow();
switch (light) {
case Red():
print('Stop');
case Yellow():
print('Slow down');
case Green():
print('Go');
}
}Slow downQuestion 7 — Solution
The program stops compiling:
Error: The type 'Light' is not exhaustively matched by the switch cases since it doesn't match 'Flashing()'.
Try adding a default case or cases that match 'Flashing()'.Add a case Flashing(): arm.
Question 8 — Solution
Circles under radius 5 match no arm (doesn't match 'Circle(radius: double())'). Add a plain
Circle() arm:
sealed class Shape {}
class Circle extends Shape {
final double radius;
Circle(this.radius);
}
class Rectangle extends Shape {
final double width, height;
Rectangle(this.width, this.height);
}
String size(Shape s) => switch (s) {
Circle(radius: >= 5) => 'big circle',
Circle() => 'small circle',
Rectangle() => 'rectangle',
};
void main() {
print(size(Circle(2)));
}small circleQuestion 9 — Solution
sealed class Result<T> {}
class Ok<T> extends Result<T> {
final T value;
Ok(this.value);
}
class Fail<T> extends Result<T> {
final String error;
Fail(this.error);
}
Result<int> safeDivide(int a, int b) =>
b == 0 ? Fail('Cannot divide by zero') : Ok(a ~/ b);
void main() {
for (var r in [safeDivide(10, 2), safeDivide(1, 0)]) {
print(switch (r) {
Ok(:var value) => 'Result: $value',
Fail(:var error) => 'Error: $error',
});
}
}Result: 5
Error: Cannot divide by zeroQuestion 10 — Solution
sealed class Move {}
class Step extends Move {
final int steps;
Step(this.steps);
}
class Jump extends Move {
final int height;
Jump(this.height);
}
class Rest extends Move {}
void main() {
var moves = [Step(3), Jump(2), Rest(), Step(1)];
var position = 0;
for (var move in moves) {
position += switch (move) {
Step(:var steps) => steps,
Jump(:var height) => height * 2,
Rest() => 0,
};
}
print('Position: $position');
}Position: 8Quick Revision
- What does
sealedtell Dart about a class family? - Why can't you write
Shape()whenShapeis sealed? - Where must a sealed class's subtypes be declared?
- Why does a switch over a sealed type not need
_? - What happens to your switches when you add a new subtype?
- When would you choose a sealed class over an enum?
Key Takeaways
sealed classdeclares a closed family: Dart knows every direct subtype.- A sealed class is implicitly abstract — only its subtypes can be created.
- Subtypes must be in the same library (file); other files can use the family but not extend or implement it.
- Switches over a sealed type are checked for exhaustiveness, with no
_needed; Dart names any missing subtype. - Object patterns like
Circle(:var radius)read each subtype's own fields safely. - Sealed families model outcomes cleanly:
Success/Failure, or a genericResult<T>withOkandFail. - Adding a subtype turns every incomplete switch into a compile error — a to-do list from the compiler.
What's Next?
Lecture 083 — Errors vs Exceptions. This closes Category 13. Result<T> returns failure as
a value; Dart can also throw it. Category 14 begins by separating errors (bugs to fix)
from exceptions (problems a program can expect and handle).