DEV Community

kanta13jp1
kanta13jp1

Posted on

Dart 3 Records and Patterns — Destructuring, Exhaustive Switches, and More

Dart 3 Records and Patterns — Destructuring, Exhaustive Switches, and More

Dart 3 shipped three related language features simultaneously: Records, Patterns, and sealed classes. Together they eliminate entire categories of boilerplate — multiple return values, verbose type checks, and non-exhaustive switch chains — without adding runtime overhead. This guide covers every feature with real Flutter code.


Records

A Record is an anonymous, fixed-size, immutable aggregate type. Unlike Map, it is fully typed.

Positional Records

// Type: (double, double)
final point = (3.0, 4.0);
print(point.$1); // 3.0
print(point.$2); // 4.0

double distance((double, double) a, (double, double) b) {
  final dx = a.$1 - b.$1;
  final dy = a.$2 - b.$2;
  return sqrt(dx * dx + dy * dy);
}
Enter fullscreen mode Exit fullscreen mode

Named Records

// Type: ({String id, String email, bool isAdmin})
final user = (id: 'u_001', email: 'alice@example.com', isAdmin: false);
print(user.id);      // u_001
print(user.isAdmin); // false
Enter fullscreen mode Exit fullscreen mode

Type Aliases

typedef Coordinate = ({double lat, double lng});
typedef ApiResult<T> = ({bool ok, T? data, String? error});

Coordinate tokyo = (lat: 35.6895, lng: 139.6917);

ApiResult<String> fetchName() =>
    (ok: true, data: 'Alice', error: null);
Enter fullscreen mode Exit fullscreen mode

Multiple Return Values (The Killer Feature)

Before Dart 3, returning multiple values required a class, a List, or a Map — all with trade-offs.

// Dart 3: clean, typed, zero boilerplate
({bool valid, String? error}) validateEmail(String input) {
  if (!input.contains('@')) return (valid: false, error: 'Missing @');
  if (input.length < 5)     return (valid: false, error: 'Too short');
  return (valid: true, error: null);
}

// Caller
final (:valid, :error) = validateEmail('alice@example.com');
if (!valid) showError(error!);
Enter fullscreen mode Exit fullscreen mode

Patterns — Destructuring

Patterns appear in variable declarations, for loops, switch expressions, switch statements, and if-case statements.

Variable Patterns

// Positional destructuring
final (x, y) = (10.0, 20.0);
print('$x, $y'); // 10.0, 20.0

// Named destructuring (shorthand when variable names match field names)
final (:String id, :String email) = (id: 'u_1', email: 'a@b.com');
print(id);    // u_1
print(email); // a@b.com
Enter fullscreen mode Exit fullscreen mode

Destructuring in for Loops

final users = [
  (id: '1', name: 'Alice', role: 'admin'),
  (id: '2', name: 'Bob',   role: 'member'),
];

for (final (:id, :name, :role) in users) {
  print('$id: $name ($role)');
}
Enter fullscreen mode Exit fullscreen mode

List Patterns

String summarize(List<int> values) => switch (values) {
  []                           => 'empty',
  [final v]                    => 'single: $v',
  [final a, final b]           => 'pair: $a, $b',
  [final first, ..., final last] => 'many; first=$first last=$last',
};

print(summarize([]));           // empty
print(summarize([7]));          // single: 7
print(summarize([1, 2]));       // pair: 1, 2
print(summarize([1, 2, 3, 4])); // many; first=1 last=4
Enter fullscreen mode Exit fullscreen mode

Map Patterns

String extractRegion(Map<String, dynamic> json) => switch (json) {
  {'country': 'JP', 'city': String city}  => 'Japan / $city',
  {'country': String c, 'city': String city} => '$c / $city',
  {'city': String city}                   => '? / $city',
  _                                       => 'Unknown',
};
Enter fullscreen mode Exit fullscreen mode

Sealed Classes and Exhaustive Switches

sealed restricts all direct subclasses to the same library, which lets the compiler verify that switch covers every case.

sealed class Shape {}

class Circle    extends Shape { final double radius; Circle(this.radius); }
class Rect      extends Shape { final double w, h;  Rect(this.w, this.h); }
class Triangle  extends Shape { final double base, height; Triangle(this.base, this.height); }

// Compiler guarantees all cases are covered — add a new subclass and it becomes a compile error
double area(Shape shape) => switch (shape) {
  Circle(:final radius)             => pi * radius * radius,
  Rect(:final w, :final h)          => w * h,
  Triangle(:final base, :final height) => 0.5 * base * height,
};
Enter fullscreen mode Exit fullscreen mode

Object Patterns

Object patterns match on type and simultaneously destructure fields.

class HttpResponse {
  final int status;
  final Map<String, dynamic> body;
  const HttpResponse(this.status, this.body);
}

String interpret(HttpResponse r) => switch (r) {
  HttpResponse(status: 200, body: {'items': List items}) =>
    'Got ${items.length} items',
  HttpResponse(status: 201, body: {'id': String id}) =>
    'Created: $id',
  HttpResponse(status: >= 400 && < 500, :final status) =>
    'Client error $status',
  HttpResponse(status: >= 500, :final status) =>
    'Server error $status',
  _ => 'Unexpected',
};
Enter fullscreen mode Exit fullscreen mode

Guards (when Clauses)

A when clause adds a boolean condition on top of a pattern match.

sealed class Tx {}
class Credit extends Tx { final double amount; Credit(this.amount); }
class Debit  extends Tx { final double amount; Debit(this.amount);  }

String classify(Tx t) => switch (t) {
  Credit(:final amount) when amount >= 10000 => 'Large credit',
  Credit(:final amount) when amount > 0      => 'Regular credit',
  Credit()                                   => 'Zero credit',
  Debit(:final amount)  when amount >= 5000  => 'Large debit ⚠️',
  Debit(:final amount)  when amount > 0      => 'Regular debit',
  Debit()                                    => 'Zero debit',
};
Enter fullscreen mode Exit fullscreen mode

if-case Statements

For single-pattern checks without a full switch.

void handleMessage(Object msg) {
  if (msg case {'type': 'chat', 'text': String text, 'from': String from}) {
    print('[$from]: $text');
  } else if (msg case {'type': 'join', 'user': String user}) {
    print('$user joined the room');
  }
}
Enter fullscreen mode Exit fullscreen mode

Real Flutter Use Cases

Typed Result Type

sealed class Result<T> {
  const Result();
}
class Ok<T>  extends Result<T> { final T value;    const Ok(this.value); }
class Err<T> extends Result<T> {
  final String message;
  final int? code;
  const Err(this.message, {this.code});
}

Widget buildFromResult(Result<List<String>> result) => switch (result) {
  Ok(value: []) =>
    const Center(child: Text('No items yet')),
  Ok(:final value) =>
    ListView.builder(
      itemCount: value.length,
      itemBuilder: (_, i) => ListTile(title: Text(value[i])),
    ),
  Err(code: 401) =>
    const Center(child: Text('Please log in')),
  Err(code: 404) =>
    const Center(child: Text('Not found')),
  Err(:final message) =>
    Center(child: Text('Error: $message')),
};
Enter fullscreen mode Exit fullscreen mode

Auth State Machine

sealed class AuthState {}
class Unauthenticated extends AuthState { const Unauthenticated(); }
class Authenticating  extends AuthState { const Authenticating();  }
class Authenticated   extends AuthState {
  final String uid;
  final String displayName;
  const Authenticated({required this.uid, required this.displayName});
}
class AuthFailed extends AuthState {
  final String reason;
  const AuthFailed(this.reason);
}

Widget routeFor(AuthState state) => switch (state) {
  Unauthenticated()           => const LoginPage(),
  Authenticating()            => const SplashPage(),
  Authenticated(:final displayName)  => HomePage(name: displayName),
  AuthFailed(:final reason)   => ErrorPage(message: reason),
};
Enter fullscreen mode Exit fullscreen mode

Combining Records and switch for Multi-Dimensional State

// (network, auth) — two independent axes
Widget buildScreen((bool online, AuthState auth) state) => switch (state) {
  (false, _)                               => const OfflineBanner(),
  (true, Unauthenticated())                => const LoginPage(),
  (true, Authenticating())                 => const SplashPage(),
  (true, Authenticated(:final displayName)) => HomePage(name: displayName),
  (true, AuthFailed(:final reason))        => ErrorPage(message: reason),
};
Enter fullscreen mode Exit fullscreen mode

Refactoring if-chains to switch Expressions

// Before — imperative, easy to miss a case
Color statusColor(String status) {
  if (status == 'active')    return Colors.green;
  if (status == 'pending')   return Colors.orange;
  if (status == 'suspended') return Colors.red;
  if (status == 'deleted')   return Colors.grey;
  return Colors.transparent;
}

// After — exhaustive, compact, readable
Color statusColor(String status) => switch (status) {
  'active'    => Colors.green,
  'pending'   => Colors.orange,
  'suspended' => Colors.red,
  'deleted'   => Colors.grey,
  _           => Colors.transparent,
};
Enter fullscreen mode Exit fullscreen mode

Quick Reference

Feature Syntax Use case
Positional record (a, b) Multiple return values
Named record (x: a, y: b) Typed lightweight DTO
Variable pattern final (x, y) = point Destructuring
Object pattern MyClass(:field) Type + field match
List pattern [first, ...rest] Structural list checks
Map pattern {'key': value} JSON/map destructuring
Sealed + switch switch (s) { SubA() => ... } Exhaustive dispatch
Guard case Foo() when condition Conditional pattern
if-case if (x case Pattern) Single-branch pattern

Dart 3's Records and Patterns make it possible to write code that is simultaneously more concise and more correct. The exhaustive switch check alone eliminates an entire class of bugs that previously only showed up at runtime.


Which pattern feature has changed how you write Flutter code most? Share your example in the comments!

Top comments (0)