What are sealed classes in Dart and how do they enable exhaustive pattern matching?
Short Answer
A sealed class restricts which classes can extend/implement it to only those defined in the same library, letting the compiler verify a switch statement handles every possible subtype — catching missing cases at compile time instead of runtime.
Before sealed classes, an abstract class could be extended anywhere, so a switch over its subtypes could never be proven exhaustive by the compiler — you always needed a default case, even after covering every known subtype, because a new one could appear from an unrelated file.
Declaring sealed class Result<T> {} with subclasses Success<T> and Failure<T> in the same file tells the compiler that's the complete set. A switch (result) { Success(:final data) => ..., Failure(:final error) => ... } is then exhaustive — if a third subclass is added later and a case is missed, the compiler errors immediately instead of silently falling through at runtime. This is the modern Dart-native alternative to encoding a result-or-error union type, replacing patterns that previously needed the freezed package.
Code Example
sealed class Result<T> {}
class Success<T> extends Result<T> { final T data; Success(this.data); }
class Failure<T> extends Result<T> { final String error; Failure(this.error); }
String describe(Result<int> result) => switch (result) {
Success(:final data) => 'Got \$data',
Failure(:final error) => 'Error: \$error',
}; // exhaustive — no default neededCommon Mistakes
- ×Adding a redundant default case to an already-exhaustive sealed-class switch, defeating the compiler's ability to catch a missed case later.
- ×Confusing sealed with abstract — sealed additionally restricts subclassing to the same library.
Interview Tips
- →Mention this reduces the need for the freezed package's union-type codegen for simple cases.