23 Sealed Classes in Dart
Sealed classes are one of Dart’s most powerful features for type-safe error handling. Think of them as a way to create a closed set of subtypes that the compiler knows about completely.
23.1 The Core Concept
In simple terms, a sealed class is a class that:
- Cannot be directly instantiated
- Can only be extended/implemented within the same library
- Forces the compiler to know all possible subtypes
Here’s a visual representation:
Regular Class Hierarchy Sealed Class Hierarchy
===================== ====================
Animal Result (sealed)
| |
|-- anywhere in |-- only in same library
| your codebase |
| |
┌───┴───┬────┬────┐ ┌──────┴──────┐
| | | | | |
Dog Cat Bird ? Success Failure
?
(can add more) (complete set!)
23.2 Basic Example
Let me show you a practical example - handling API responses:
// Define the sealed class
sealed class Result<T> {
const Result();
}
// All subtypes must be in the same file
final class Success<T> extends Result<T> {
final T data;
const Success(this.data);
}
final class Failure<T> extends Result<T> {
final String error;
const Failure(this.error);
}23.3 The Magic: Exhaustive Pattern Matching
Now here’s where sealed classes shine. When you use pattern matching with a sealed class, the compiler forces you to handle ALL cases:
String handleResult(Result<String> result) {
return switch (result) {
Success(data: var d) => 'Got data: $d',
Failure(error: var e) => 'Error: $e',
// If you forget a case, you get a COMPILE-TIME error!
};
}If you try to omit one of the cases:
String handleResult(Result<String> result) {
return switch (result) {
Success(data: var d) => 'Got data: $d',
// Compiler error: "The type 'Result<String>' is not exhaustively matched"
};
}23.4 Compare with Regular Classes
With a regular abstract class, this wouldn’t work:
// Regular abstract class
abstract class ApiResponse {
const ApiResponse();
}
class Loading extends ApiResponse {}
class Success extends ApiResponse {}
class Error extends ApiResponse {}
// Someone in another file could add:
class SomeOtherCase extends ApiResponse {}
// The compiler can't enforce exhaustiveness!
String handle(ApiResponse response) {
return switch (response) {
Loading() => 'loading',
Success() => 'success',
// Compiler doesn't know about all possible subtypes
};
}23.5 Real-World Pattern: State Management
Here’s a practical example you might use in Flutter - representing UI states:
sealed class LoadingState<T> {
const LoadingState();
}
final class Initial<T> extends LoadingState<T> {
const Initial();
}
final class Loading<T> extends LoadingState<T> {
const Loading();
}
final class Loaded<T> extends LoadingState<T> {
final T data;
const Loaded(this.data);
}
final class LoadError<T> extends LoadingState<T> {
final String message;
final Exception? exception;
const LoadError(this.message, [this.exception]);
}
// In your Flutter widget:
Widget buildContent(LoadingState<List<Patient>> state) {
return switch (state) {
Initial() => Text('Tap to load patients'),
Loading() => CircularProgressIndicator(),
Loaded(data: var patients) => PatientList(patients),
LoadError(message: var msg) => ErrorWidget(msg),
// Must handle all cases - compiler enforces this!
};
}23.6 Connection to Your Python Experience
If you’ve used Python’s typing with Union types and match statements (Python 3.10+), sealed classes are similar but compiler-enforced:
# Python (not enforced at compile time)
from typing import Union
from dataclasses import dataclass
@dataclass
class Success:
data: str
@dataclass
class Failure:
error: str
Result = Union[Success, Failure]
# mypy can check this, but not enforced at runtime
def handle(result: Result) -> str:
match result:
case Success(data):
return f"Got: {data}"
# If you forget Failure, mypy warns but Python runsDart’s sealed classes give you this pattern with compile-time guarantees.
23.7 Key Benefits for Your Radiology AI Work
- API Response Handling: Model different response states type-safely
- Image Processing States: Represent processing pipeline states
- Validation Results: Handle validation with all edge cases covered
- Error Types: Create domain-specific error hierarchies
Example for medical imaging:
sealed class DicomLoadResult {
const DicomLoadResult();
}
final class DicomLoaded extends DicomLoadResult {
final DicomImage image;
final DicomMetadata metadata;
const DicomLoaded(this.image, this.metadata);
}
final class DicomCorrupted extends DicomLoadResult {
final String reason;
const DicomCorrupted(this.reason);
}
final class DicomNotFound extends DicomLoadResult {
final String path;
const DicomNotFound(this.path);
}
final class DicomUnauthorized extends DicomLoadResult {
const DicomUnauthorized();
}Now when you handle DICOM loading, the compiler forces you to think about all error cases - no forgotten edge cases that could crash your radiology app!
23.8 Examples
23.8.1 Result Type
File: lib/core/result.dart (NEW)
Implementation:
/// Represents the result of an operation that can succeed or fail
sealed class Result<T, E> {
const Result();
}
/// Success case containing the result value
final class Success<T, E> extends Result<T, E> {
final T value;
const Success(this.value);
}
/// Failure case containing the error
final class Failure<T, E> extends Result<T, E> {
final E error;
const Failure(this.error);
}
// Extension methods for convenience
extension ResultExtensions<T, E> on Result<T, E> {
bool get isSuccess => this is Success<T, E>;
bool get isFailure => this is Failure<T, E>;
T? get valueOrNull => switch (this) {
Success(value: final v) => v,
Failure() => null,
};
E? get errorOrNull => switch (this) {
Success() => null,
Failure(error: final e) => e,
};
}23.8.2 Calculator Error Types
File: lib/services/calculator/calculator_error.dart (NEW)
Error Hierarchy:
/// Base sealed class for calculator errors
sealed class CalculatorError {
final String message;
const CalculatorError(this.message);
}
/// Input parsing failed (non-numeric, invalid format)
final class ParseError extends CalculatorError {
final String fieldName;
final String input;
ParseError(this.fieldName, this.input)
: super('$fieldName must be a valid number');
}
/// Business rule validation failed
final class ValidationError extends CalculatorError {
ValidationError(String message) : super(message);
}
/// Mathematical operation error
final class CalculationError extends CalculatorError {
CalculationError(String message) : super(message);
}