What are sealed classes in Dart?
3 min read
Dart 3 / OOP
| Modifier | Subclassable | Subtypes must be in same library? | Implicitly abstract? |
|---|---|---|---|
| (none) | ✅ Yes | ❌ | ❌ |
abstract | ✅ Yes | ❌ | ✅ |
sealed | ✅ Yes | ✅ Yes | ✅ |
final | ❌ No | n/a | optional |
base | ✅ subclasses must be base/final/sealed | ❌ | optional |
Sealed = "I know all my subtypes at compile time." The compiler uses that knowledge to make switch exhaustive without a default branch.
Code in action
sealed class Result<T> {}
class Success<T> extends Result<T> { final T value; Success(this.value); }
class Failure<T> extends Result<T> { final String error; Failure(this.error); }
class Loading<T> extends Result<T> {}
// Exhaustive — no `default` needed, compiler covers all cases
String describe(Result<String> r) => switch (r) {
Success(:final value) => 'Got $value',
Failure(:final error) => 'Oops: $error',
Loading() => 'Loading…',
};
If you add class Cached<T> extends Result<T> and forget to update describe, the build fails — the compiler points you at every switch. That's the whole point.
Killer use case: app state
sealed class AuthState {}
class AuthIdle extends AuthState {}
class AuthLoading extends AuthState {}
class Authenticated extends AuthState { final User user; Authenticated(this.user); }
class AuthFailed extends AuthState { final String msg; AuthFailed(this.msg); }
Widget build(AuthState s) => switch (s) {
AuthIdle() => const LoginScreen(),
AuthLoading() => const Spinner(),
Authenticated(:final user) => HomeScreen(user: user),
AuthFailed(:final msg) => ErrorBanner(msg),
};
Same shape works beautifully for ApiResponse<T>, NavigationEvent, FormFieldState, etc.
Sealed vs abstract vs enum
| You need… | Use |
|---|---|
| Closed set of states, each carrying different data | sealed class |
| Closed set of states with no data (statuses, flags) | enum |
| Open hierarchy (third parties can subclass) | abstract class |
| Lock down the class entirely (no subclassing anywhere) | final class |
Common mistakes to avoid
// ❌ Sealed class with subtypes in a different file
// lib/auth/state.dart
sealed class AuthState {}
// lib/auth/login.dart
class LoginSuccess extends AuthState {} // ❌ must be in the same library
// ✅ Use part files or keep them in the same file
// ❌ Using `default:` and missing the exhaustiveness benefit
switch (state) {
case AuthIdle(): ...
default: throw 'unreachable'; // hides missing cases from the compiler
}
// ✅ Drop the default — the compiler will warn if you miss a case
// ❌ Treating it like a normal abstract class with public constructors
sealed class Result {
Result(); // ok, but...
}
class External {} // anywhere outside the lib
// extends Result not possible — but that's the feature, not a bug
// ❌ Sealing when you don't switch on it
sealed class Animal {} // adds friction with no benefit
// ✅ Only seal when exhaustive switch / pattern matching pays off
// ❌ Forgetting to handle a destructure with `:final` for nullable fields
case Authenticated(:final user) when user.isAdmin => ...;
// fine, but the type system still tracks user as non-null because Authenticated declared it
Interview follow-ups
-
What problem do sealed classes solve over plain abstract classes? Exhaustiveness. A plain
abstract classcan be extended anywhere, so the compiler can't know all subtypes —switchalways needs adefault.sealedcloses the set, enabling compile-time guarantees that every case is handled. -
Why must sealed subclasses live in the same library? So the compiler can prove the set is closed. If you could extend a sealed class from another package, the compiler would have no way to enumerate subtypes — and the exhaustiveness guarantee would silently break for downstream consumers.
-
When should you use a sealed class vs an enum?
enumfor finite states with no payload (Status.idle,Status.done).sealed classwhen each case carries different data (a success has a value, a failure has an error). Dart 3 enhanced enums blur the line, but rule of thumb: payload → sealed, label → enum. -
What's the difference between
sealed,final, andbaseclass modifiers?sealed= abstract + closed subtype set.final= nobody can extend or implement it at all.base= can be extended but only by classes that are alsobase/final/sealed(prevents accidentalimplements-only subtypes). They're orthogonal tools for class-level access control.
How helpful was this content?
Please sign in to rate this article.