Records, Patterns, Sealed Classes & Class Modifiers
Modern Dart 3.x — The Features Interviewers Love to Ask
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Dart 3 introduced records (lightweight multi-value returns), patterns (destructuring and matching), sealed classes (closed type hierarchies with exhaustive checking), and class modifiers (base, interface, final, mixin). These features modernize Dart significantly and are increasingly tested in senior interviews.
Real-world relevance
In a fintech app handling refund claims, sealed classes model the claim lifecycle: sealed class ClaimState {}. class Draft extends ClaimState {}, class Submitted extends ClaimState { final DateTime at; }, class Approved extends ClaimState { final double amount; }, class Rejected extends ClaimState { final String reason; }. Exhaustive switch ensures every state renders correctly in the UI with no missing cases.
Key points
- Records — Lightweight Data Bundles — Records bundle multiple values without creating a class: (String, int) record = ('Alice', 25); Named fields: ({String name, int age}) person = (name: 'Alice', age: 25); Records are value types — two records with the same values are ==. Perfect for returning multiple values from functions.
- Pattern Matching with switch — Dart 3 switch expressions match on type, value, and structure: switch (shape) { Circle(radius: var r) => pi * r * r, Square(side: var s) => s * s }. Exhaustive checking ensures you handle all cases. This replaces long if-else chains.
- Destructuring with Patterns — final (name, age) = getUserInfo(); instantly unpacks a record. var {'name': name, 'age': age} = json; destructures a map. if (value case int x when x > 0) { ... } combines type check + binding + guard. Extremely powerful for JSON parsing.
- Sealed Classes — sealed class Shape {} class Circle extends Shape { ... } class Square extends Shape { ... }. Sealed classes restrict which classes can extend them (same file only). The compiler knows ALL subtypes, enabling exhaustive switch. Like a type-safe union/enum with data.
- Exhaustive Switch with Sealed — When you switch on a sealed class, the compiler warns if you miss a case. No need for 'default'. This prevents bugs when adding new subtypes — the compiler forces you to handle them everywhere. This is the #1 reason to use sealed classes.
- Class Modifiers: base, interface, final, mixin — Dart 3 added: base (can extend, can't implement), interface (can implement, can't extend), final (can't extend or implement outside library), mixin class (can be used as both class and mixin). These control how your classes are used by other code.
- When to Use Sealed vs Enum — Enum: fixed set of values, all same type, no data variation. Sealed: fixed set of subtypes, each can carry different data. Use enum for Status.loading/success/error. Use sealed when each state needs different fields: LoadedState(data), ErrorState(message).
- Guard Clauses in Patterns — Add 'when' guards: case int x when x > 0 => 'positive'. Guards add extra conditions to pattern matches. Useful for complex validation logic in switch expressions. Interview: How do you handle overlapping patterns?
- Practical Patterns for State — BLoC states are perfect for sealed classes: sealed class AuthState {}. class Authenticated extends AuthState { final User user; }. class Unauthenticated extends AuthState {}. class AuthLoading extends AuthState {}. Exhaustive switch in UI ensures all states are rendered.
- Object Patterns for JSON — Parse JSON safely: if (json case {'name': String name, 'age': int age}) { ... }. This checks the structure AND extracts values in one step. Much safer than manual json['name'] as String which crashes on wrong types.
Code example
// Records, Patterns, Sealed Classes — Dart 3.x
// --- RECORDS ---
// Positional record
(String, int) getUser() => ('Alice', 25);
// Named record fields
({String name, int age, String email}) getUserInfo() =>
(name: 'Alice', age: 25, email: 'alice@mail.com');
// Destructuring records
void example() {
final (name, age) = getUser();
print('$name is $age'); // Alice is 25
final (:name, :age, :email) = getUserInfo(); // Named destructuring
print('$name ($email)');
}
// --- PATTERN MATCHING ---
// Switch expression (Dart 3)
String describe(Object value) => switch (value) {
int x when x > 0 => 'Positive int: $x',
int x when x < 0 => 'Negative int: $x',
0 => 'Zero',
String s => 'String of length ${s.length}',
(int a, int b) => 'Record: ($a, $b)',
_ => 'Unknown type',
};
// Object pattern for JSON parsing
void parseJson(Map<String, dynamic> json) {
if (json case {'name': String name, 'age': int age, 'scores': List scores}) {
print('Name: $name, Age: $age, Scores: $scores');
} else {
print('Invalid JSON structure');
}
}
// --- SEALED CLASSES ---
sealed class NetworkResult<T> {
const NetworkResult();
}
class Success<T> extends NetworkResult<T> {
final T data;
const Success(this.data);
}
class Failure<T> extends NetworkResult<T> {
final String message;
final int? statusCode;
const Failure(this.message, {this.statusCode});
}
class Loading<T> extends NetworkResult<T> {
const Loading();
}
// Exhaustive switch — compiler warns if you miss a case!
Widget buildUI(NetworkResult<User> result) => switch (result) {
Success(:final data) => UserCard(user: data),
Failure(:final message) => ErrorWidget(message: message),
Loading() => const CircularProgressIndicator(),
// No default needed! Compiler knows all cases.
};
// --- SEALED FOR BLOC STATES ---
sealed class AuthState {
const AuthState();
}
class AuthInitial extends AuthState {
const AuthInitial();
}
class AuthLoading extends AuthState {
const AuthLoading();
}
class Authenticated extends AuthState {
final User user;
final String token;
const Authenticated({required this.user, required this.token});
}
class Unauthenticated extends AuthState {
final String? reason;
const Unauthenticated({this.reason});
}
// --- CLASS MODIFIERS ---
base class Animal {} // Can extend, can't implement
interface class Printable {} // Can implement, can't extend
final class Config {} // Can't extend or implement outside file
mixin class Validator {} // Can be used as class or mixinLine-by-line walkthrough
- 1. Positional record — returns two values without creating a class
- 2. Named record fields — more readable, accessed by name
- 3. Destructuring a positional record into separate variables
- 4. Named destructuring with :name syntax — extracts matching fields
- 5. Switch expression — pattern matches on type and value
- 6. Guard clause with 'when' — matches only when condition is true
- 7. Object pattern — checks JSON structure AND extracts values in one step
- 8. Sealed class definition — only subtypes in this file are allowed
- 9. Success subtype carries generic data
- 10. Failure subtype carries message and optional status code
- 11. Exhaustive switch — compiler ensures all subtypes are handled
- 12. Destructuring in switch case — ':final data' extracts the data field
- 13. Sealed class for BLoC state — each state carries different data
- 14. Class modifiers control how types can be used externally
Spot the bug
sealed class AppState {}
class Loading extends AppState {}
class Loaded extends AppState { final List<String> items; Loaded(this.items); }
class Error extends AppState { final String message; Error(this.message); }
String render(AppState state) => switch (state) {
Loading() => 'Loading...',
Loaded(:final items) => 'Got ${items.length} items',
};Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Records (Dart Official)
- Patterns (Dart Official)
- Sealed Classes (Dart Official)
- Class Modifiers (Dart Official)