Sealed Classes
Understand how to use sealed classes for exhaustive pattern matching in Dart.
What is it?
Sealed classes are a feature introduced in Dart 3 that allow you to define a restricted hierarchy of classes. A sealed class can only be extended or implemented within the same file, enabling exhaustive checking in switch statements and pattern matching.
Why does it exist?
Sealed classes exist to:
- Enable exhaustive pattern matching
- Create closed type hierarchies
- Enforce complete handling of all cases
- Improve type safety
- Support algebraic data types
- Prevent unwanted subclassing
Basic Sealed Classes
Simple Sealed Class
// Sealed class (can only be extended in this file)
sealed class Result<T> {
// Can have fields and methods
const Result();
bool get isSuccess => this is Success<T>;
bool get isError => this is Error<T>;
bool get isLoading => this is Loading<T>;
}
class Success<T> extends Result<T> {
final T data;
const Success(this.data);
@override
String toString() => 'Success($data)';
}
class Error<T> extends Result<T> {
final String message;
const Error(this.message);
@override
String toString() => 'Error($message)';
}
class Loading<T> extends Result<T> {
const Loading();
@override
String toString() => 'Loading...';
}
// Usage - Exhaustive pattern matching
String handleResult<T>(Result<T> result) {
return switch (result) {
Success(data: var data) => 'Success: $data',
Error(message: var msg) => 'Error: $msg',
Loading() => 'Loading...',
};
}
// Usage
var success = Success('Data loaded');
var error = Error('Something went wrong');
var loading = Loading();
print(handleResult(success)); // Success: Data loaded
print(handleResult(error)); // Error: Something went wrong
print(handleResult(loading)); // Loading...
Sealed Class with Data
sealed class ApiResponse {
const ApiResponse();
}
class SuccessResponse extends ApiResponse {
final Map<String, dynamic> data;
final int statusCode;
const SuccessResponse(this.data, this.statusCode);
}
class ErrorResponse extends ApiResponse {
final String message;
final int statusCode;
const ErrorResponse(this.message, this.statusCode);
}
class LoadingResponse extends ApiResponse {
const LoadingResponse();
}
class TimeoutResponse extends ApiResponse {
const TimeoutResponse();
}
// Handling all cases
void processApiResponse(ApiResponse response) {
switch (response) {
case SuccessResponse(data: var data, statusCode: var code):
print('Success ($code): $data');
case ErrorResponse(message: var msg, statusCode: var code):
print('Error ($code): $msg');
case LoadingResponse():
print('Loading...');
case TimeoutResponse():
print('Request timed out');
}
}
// Usage
var success = SuccessResponse({'id': 1, 'name': 'Alice'}, 200);
var error = ErrorResponse('Not found', 404);
var loading = LoadingResponse();
var timeout = TimeoutResponse();
processApiResponse(success);
// Success (200): {id: 1, name: Alice}
processApiResponse(error);
// Error (404): Not found
processApiResponse(loading);
// Loading...
processApiResponse(timeout);
// Request timed out
Sealed Classes with Methods
Including Behavior
sealed class Shape {
const Shape();
// Abstract methods
double get area;
double get perimeter;
String get description;
// Concrete method
bool get isRegular => false;
// Helper methods
String get shortDescription => '$runtimeType: ${description}';
}
class Circle extends Shape {
final double radius;
const Circle(this.radius);
@override
double get area => math.pi * radius * radius;
@override
double get perimeter => 2 * math.pi * radius;
@override
String get description => 'Circle with radius $radius';
@override
bool get isRegular => true;
}
class Rectangle extends Shape {
final double width;
final double height;
const Rectangle(this.width, this.height);
@override
double get area => width * height;
@override
double get perimeter => 2 * (width + height);
@override
String get description => 'Rectangle $width x $height';
@override
bool get isRegular => width == height;
}
class Square extends Shape {
final double side;
const Square(this.side);
@override
double get area => side * side;
@override
double get perimeter => 4 * side;
@override
String get description => 'Square with side $side';
@override
bool get isRegular => true;
}
// Usage
void processShape(Shape shape) {
// Pattern matching with guards
switch (shape) {
case Circle(radius: var r) when r > 10:
print('Large circle: $r');
case Circle(radius: var r):
print('Circle: $r');
case Rectangle(width: var w, height: var h) when w == h:
print('Rectangle (square): $w x $h');
case Rectangle(width: var w, height: var h):
print('Rectangle: $w x $h');
case Square(side: var s):
print('Square: $s');
}
// Using methods
print('Area: ${shape.area}');
print('Perimeter: ${shape.perimeter}');
print('Regular: ${shape.isRegular}');
print('---');
}
var shapes = [
Circle(5),
Rectangle(10, 20),
Square(15),
Circle(20),
];
for (var shape in shapes) {
processShape(shape);
}
Sealed Classes with Generics
Generic Sealed Classes
sealed class Either<L, R> {
const Either();
bool get isLeft => this is Left<L, R>;
bool get isRight => this is Right<L, R>;
// Methods for handling either value
T fold<T>(T Function(L) leftFn, T Function(R) rightFn) {
return switch (this) {
Left(value: var l) => leftFn(l),
Right(value: var r) => rightFn(r),
};
}
// Helper methods
L? get leftOrNull => isLeft ? (this as Left<L, R>).value : null;
R? get rightOrNull => isRight ? (this as Right<L, R>).value : null;
}
class Left<L, R> extends Either<L, R> {
final L value;
const Left(this.value);
@override
String toString() => 'Left($value)';
}
class Right<L, R> extends Either<L, R> {
final R value;
const Right(this.value);
@override
String toString() => 'Right($value)';
}
// Usage
Either<String, int> parseNumber(String input) {
try {
var value = int.parse(input);
return Right(value);
} catch (e) {
return Left('Invalid number: $input');
}
}
void processResult(Either<String, int> result) {
// Using fold
String message = result.fold(
(left) => 'Error: $left',
(right) => 'Success: $right',
);
print(message);
// Pattern matching
switch (result) {
case Left(value: var error):
print('Error: $error');
case Right(value: var number):
print('Number: $number');
}
}
// Usage
var result1 = parseNumber('123');
var result2 = parseNumber('abc');
processResult(result1); // Success: 123, Number: 123
processResult(result2); // Error: Invalid number: abc, Error: Invalid number: abc
// Using helper methods
print(result1.rightOrNull); // 123
print(result2.rightOrNull); // null
print(result1.leftOrNull); // null
print(result2.leftOrNull); // Invalid number: abc
Sealed Classes vs Enums
Comparison
// Enum (simple values)
enum Status { pending, processing, completed, failed }
// Sealed class (with data)
sealed class TaskStatus {
const TaskStatus();
}
class Pending extends TaskStatus {
final DateTime createdAt;
const Pending(this.createdAt);
}
class Processing extends TaskStatus {
final double progress;
const Processing(this.progress);
}
class Completed extends TaskStatus {
final DateTime completedAt;
const Completed(this.completedAt);
}
class Failed extends TaskStatus {
final String error;
final DateTime failedAt;
const Failed(this.error, this.failedAt);
}
// Usage with enum
String getStatusMessage(Status status) {
return switch (status) {
Status.pending => 'Waiting...',
Status.processing => 'Processing...',
Status.completed => 'Done!',
Status.failed => 'Failed!',
};
}
// Usage with sealed class
String getTaskStatusMessage(TaskStatus status) {
return switch (status) {
Pending(createdAt: var time) => 'Created at $time',
Processing(progress: var p) => 'Processing: ${(p * 100).toStringAsFixed(0)}%',
Completed(completedAt: var time) => 'Completed at $time',
Failed(error: var err, failedAt: var time) => 'Failed: $err at $time',
};
}
// Usage
var pending = Pending(DateTime.now());
var processing = Processing(0.75);
var completed = Completed(DateTime.now());
var failed = Failed('Network error', DateTime.now());
print(getTaskStatusMessage(pending)); // Created at 2024-01-01 00:00:00.000
print(getTaskStatusMessage(processing)); // Processing: 75%
print(getTaskStatusMessage(completed)); // Completed at 2024-01-01 00:00:00.001
print(getTaskStatusMessage(failed)); // Failed: Network error at 2024-01-01 00:00:00.001
Best Practices
Use for State Management
// Good: State management with sealed classes
sealed class UIState {
const UIState();
}
class InitialState extends UIState {
const InitialState();
}
class LoadingState extends UIState {
const LoadingState();
}
class LoadedState extends UIState {
final List<String> items;
const LoadedState(this.items);
}
class ErrorState extends UIState {
final String message;
const ErrorState(this.message);
}
class EmptyState extends UIState {
const EmptyState();
}
void handleState(UIState state) {
switch (state) {
case InitialState():
print('Initial state');
case LoadingState():
print('Loading...');
case LoadedState(items: var items):
print('Loaded ${items.length} items');
case ErrorState(message: var msg):
print('Error: $msg');
case EmptyState():
print('No items to display');
}
}
Use Exhaustive Pattern Matching
// Good: Exhaustive pattern matching
String processResult<T>(Result<T> result) {
return switch (result) {
Success(data: var data) => 'Success: $data',
Error(message: var msg) => 'Error: $msg',
Loading() => 'Loading...',
};
}
// Bad: Non-exhaustive handling
String processResultBad<T>(Result<T> result) {
if (result is Success) {
return 'Success';
}
// Missing Error and Loading cases
}
Common Mistakes
Subclassing Outside File
Wrong:
// In another file
class CustomResult<T> extends Result<T> {
// Error: Sealed class can only be extended in same file
}
Correct:
// Extend only in the same file where sealed class is defined
sealed class Result<T> {
// All subclasses defined in same file
}
class Success<T> extends Result<T> { /* ... */ }
class Error<T> extends Result<T> { /* ... */ }
Missing Cases in Switch
Wrong:
String processResult<T>(Result<T> result) {
return switch (result) {
Success(data: var data) => 'Success: $data',
Error(message: var msg) => 'Error: $msg',
// Missing Loading case - Warning/Error!
};
}
Correct:
String processResult<T>(Result<T> result) {
return switch (result) {
Success(data: var data) => 'Success: $data',
Error(message: var msg) => 'Error: $msg',
Loading() => 'Loading...',
};
}
Summary
Sealed classes provide a powerful way to create closed type hierarchies with exhaustive pattern matching. They're ideal for state management, result types, and algebraic data types.
Next Steps
Now that you understand sealed classes, continue to:
Did You Know?
- Sealed classes were introduced in Dart 3
- They can only be extended in the same file
- Sealed classes enable exhaustive pattern matching
- They support generic types
- Sealed classes can have methods and fields
- They work great with switch expressions
- Sealed classes are more powerful than enums for complex cases