Sealed Classes and Exhaustive Switching
Prerequisites: Patterns and destructuring, Enums
Enums work when every option is a single value. When each option needs to carry different data, use a sealed class: a closed family of subclasses that the compiler knows completely.
Example: modeling results
sealed class PaymentResult {}
class Success extends PaymentResult {
final String transactionId;
Success(this.transactionId);
}
class Declined extends PaymentResult {
final String reason;
Declined(this.reason);
}
class Pending extends PaymentResult {}
String message(PaymentResult result) => switch (result) {
Success(:final transactionId) => 'Paid (ref $transactionId)',
Declined(:final reason) => 'Declined: $reason',
Pending() => 'Waiting for the bank...',
};
void main() {
print(message(Success('TX-1042')));
print(message(Declined('insufficient funds')));
print(message(Pending()));
}
Expected output
Paid (ref TX-1042)
Declined: insufficient funds
Waiting for the bank...
- A
sealedclass can't be created directly, and it can only be subclassed in the same file — so the compiler knows every subtype. - That makes the
switchexhaustive with no_case. Add a new subtype, and every switch that doesn't handle it becomes a compile error — exactly where your attention is needed. Success(:final transactionId)matches the type and reads the field.
Making impossible states impossible
Compare screen state modeled with separate nullable fields — where
data and error could both be set by mistake — with a sealed type:
sealed class ScreenState {}
class Loading extends ScreenState {}
class Loaded extends ScreenState {
final List<String> items;
Loaded(this.items);
}
class Failed extends ScreenState {
final String message;
Failed(this.message);
}
String render(ScreenState state) => switch (state) {
Loading() => 'Spinner',
Loaded(:final items) when items.isEmpty => 'Nothing here yet',
Loaded(:final items) => 'Showing ${items.length} items',
Failed(:final message) => 'Error: $message',
};
void main() {
print(render(Loading()));
print(render(Loaded([])));
print(render(Loaded(['Ada', 'Linus'])));
print(render(Failed('offline')));
}
Spinner
Nothing here yet
Showing 2 items
Error: offline
This pattern is common in Flutter apps: a screen's state is exactly one of these cases, and the UI switches on it.
Practice
Define sealed class Shape with Circle(radius) and
Rectangle(width, height) subclasses, and an area function using a
switch expression. Print both areas with two decimal places for a circle
of radius 1 and a 2 by 3 rectangle.
Show solution
import 'dart:math';
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 area(Shape shape) => switch (shape) {
Circle(:final radius) => pi * radius * radius,
Rectangle(:final width, :final height) => width * height,
};
void main() {
print(area(Circle(1)).toStringAsFixed(2));
print(area(Rectangle(2, 3)).toStringAsFixed(2));
}
3.14
6.00