DEV Community

kanta13jp1
kanta13jp1

Posted on

Dart Records & Patterns Deep Dive — Destructuring, Sealed Classes & Exhaustive Matching

Dart Records & Patterns Deep Dive — Destructuring, Sealed Classes & Exhaustive Matching

Dart 3.0 shipped Records, Patterns, and Sealed Classes together. Used well, they eliminate entire categories of runtime errors and make state management dramatically more expressive.

Records — typed tuples without the boilerplate

// Before: untyped Map
Map<String, dynamic> getUserInfo() => {'name': 'Alice', 'age': 30};

// Dart 3: typed Record
(String name, int age) getUserInfo() => ('Alice', 30);

void main() {
  final (name, age) = getUserInfo();
  print('$name is $age years old');
}
Enter fullscreen mode Exit fullscreen mode

Named fields

({String name, int age, String email}) getUser() => (
  name: 'Alice',
  age: 30,
  email: 'alice@example.com',
);

final user = getUser();
print(user.name); // Alice
Enter fullscreen mode Exit fullscreen mode

Parsing Edge Function responses with Records

({bool ok, String? error, Map<String, dynamic>? data}) parseEfResponse(
    http.Response response) {
  if (response.statusCode == 200) {
    return (ok: true, error: null, data: jsonDecode(response.body));
  }
  return (ok: false, error: response.body, data: null);
}

Future<void> fetchDashboard() async {
  final result = parseEfResponse(await http.get(Uri.parse('...')));
  if (!result.ok) {
    print('Error: ${result.error}');
    return;
  }
  processData(result.data!);
}
Enter fullscreen mode Exit fullscreen mode

Patterns — destructuring as a first-class feature

List patterns

switch ([1, 2, 3, 4, 5]) {
  case [var first, var second, ...]:
    print('First: $first, Second: $second');
  case []:
    print('Empty');
}
Enter fullscreen mode Exit fullscreen mode

Map patterns

void processConfig(Map<String, dynamic> config) {
  switch (config) {
    case {'type': 'ai_chat', 'model': String model, 'temperature': double temp}:
      initAiChat(model: model, temperature: temp);
    case {'type': 'competitor_check', 'urls': List urls}:
      checkCompetitors(urls.cast<String>());
    default:
      throw ArgumentError('Unknown config type');
  }
}
Enter fullscreen mode Exit fullscreen mode

Object patterns

sealed class Shape {}
class Circle extends Shape { final double radius; Circle(this.radius); }
class Rectangle extends Shape {
  final double width, height;
  Rectangle(this.width, this.height);
}

double area(Shape shape) => switch (shape) {
  Circle(:final radius) => math.pi * radius * radius,
  Rectangle(:final width, :final height) => width * height,
};
Enter fullscreen mode Exit fullscreen mode

Sealed Classes — compiler-enforced exhaustiveness

sealed class AgentState {}

class AgentIdle extends AgentState {}
class AgentThinking extends AgentState {
  final String task;
  AgentThinking(this.task);
}
class AgentCompleted extends AgentState {
  final String result;
  final Duration elapsed;
  AgentCompleted(this.result, this.elapsed);
}
class AgentFailed extends AgentState {
  final String error;
  final int retryCount;
  AgentFailed(this.error, this.retryCount);
}

// Compiler guarantees all cases are covered
String describeState(AgentState state) => switch (state) {
  AgentIdle() => 'Idle',
  AgentThinking(:final task) => 'Processing: $task',
  AgentCompleted(:final result, :final elapsed) =>
    'Done in ${elapsed.inSeconds}s: $result',
  AgentFailed(:final error, :final retryCount) =>
    'Failed (attempt $retryCount): $error',
};
Enter fullscreen mode Exit fullscreen mode

Driving Flutter UI from sealed state

Widget buildAgentWidget(AgentState state) => switch (state) {
  AgentIdle() => const Icon(Icons.smart_toy_outlined),
  AgentThinking(:final task) => Column(children: [
    const CircularProgressIndicator(),
    Text(task),
  ]),
  AgentCompleted(:final result) =>
    Text(result, style: const TextStyle(color: Colors.green)),
  AgentFailed(:final error, :final retryCount) => Column(children: [
    const Icon(Icons.error, color: Colors.red),
    Text(error),
    if (retryCount < 3)
      TextButton(onPressed: retry, child: const Text('Retry')),
  ]),
};
Enter fullscreen mode Exit fullscreen mode

Guard clauses with when

List<String> categorize(List<int> numbers) => [
  for (final n in numbers)
    switch (n) {
      int x when x < 0 => 'negative',
      0 => 'zero',
      int x when x.isEven => 'positive even',
      _ => 'positive odd',
    }
];
Enter fullscreen mode Exit fullscreen mode

Pattern: a type-safe Result type

sealed class Result<T> {}

class Ok<T> extends Result<T> {
  final T value;
  Ok(this.value);
}

class Err<T> extends Result<T> {
  final String message;
  final StackTrace? stackTrace;
  Err(this.message, [this.stackTrace]);
}

Result<List<Map<String, dynamic>>> parseJson(String body) {
  try {
    final data = jsonDecode(body) as List;
    return Ok(data.cast<Map<String, dynamic>>());
  } catch (e, st) {
    return Err('JSON parse failed: $e', st);
  }
}

Future<void> loadAiProviders() async {
  switch (parseJson(await fetchProviders())) {
    case Ok(:final value):
      setState(() => providers = value);
    case Err(:final message, :final stackTrace):
      logger.error(message, stackTrace);
      showSnackBar('Failed to load');
  }
}
Enter fullscreen mode Exit fullscreen mode

Quick reference

Feature Best for
Records Multiple return values, lightweight DTOs, destructuring
List/Map/Object Patterns Data-shape branching and transformation
Sealed Classes State machines, error types, algebraic data types
Guard clauses (when) Conditional patterns
if-case Single destructure + type narrowing

Dart 3 patterns eliminate most instanceof checks, casts, and null guards — and the compiler tells you immediately when you miss a case.

Top comments (0)