Records, patterns and switch expressions
Return multiple values with records, destructure them with patterns, and use sealed classes for exhaustive switches.
Records and destructuring
// a positional record: a lightweight tuple with a real type
(int, String) pair = (1, 'one');
print(pair.$1); // 1
print(pair.$2); // 'one'
// named fields, which are self-documenting
({int status, String body}) response = (status: 200, body: 'ok');
print(response.status);
// records have structural equality and a useful toString
print((1, 'a') == (1, 'a')); // true
print((1, 2).toString()); // (1, 2)
// a record as a return type: no more out-parameters or a bespoke class
({double min, double max}) range(List<double> xs) {
var lo = xs.first, hi = xs.first;
for (final x in xs) {
if (x < lo) lo = x;
if (x > hi) hi = x;
}
return (min: lo, max: hi);
}
// destructuring with patterns
void main() {
final r = range([3.0, 1.0, 4.0, 1.5]);
final (lo, hi) = (r.min, r.max); // positional destructuring
final (:min, :max) = r; // shorthand for named fields
print('$lo $hi $min $max');
final (a, b) = (1, 2);
print(a + b);
// swapping without a temporary
var x = 1, y = 2;
(x, y) = (y, x);
print('$x $y');
// list and map patterns
final [first, second, ...rest] = [1, 2, 3, 4];
print('$first $second $rest');
final {'name': name, 'age': age} = {'name': 'ada', 'age': 36};
print('$name $age');
}- A record is immutable and has no methods. Use it for a short-lived grouping of values, not as a replacement for a domain class with behaviour.
- Records are structural:
(1, 'a')and(1, 'a')are equal. Two instances of a plain class with the same fields are not, unless you override==. - The positional accessors are
$1,$2and so on. In a string template you must escape the dollar:'${pair.$1}'. - Do not use a record to return five values. Once a grouping has a name and a meaning, promote it to a class or a typedef of a record.
Switch expressions and pattern matching
sealed class Shape {}
class Circle extends Shape {
Circle(this.radius);
final double radius;
}
class Rectangle extends Shape {
Rectangle(this.width, this.height);
final double width;
final double height;
}
class Triangle extends Shape {
Triangle(this.base, this.height);
final double base;
final double height;
}
double area(Shape s) => switch (s) {
Circle(:final radius) => 3.14159 * radius * radius,
Rectangle(:final width, :final height) => width * height,
Triangle(base: final b, height: final h) => 0.5 * b * h,
// no default needed: the compiler knows Shape is sealed
};
String describe(Object? value) => switch (value) {
null => 'nothing',
int n when n < 0 => 'negative $n',
int n => 'int $n',
double d => 'double $d',
String s when s.isEmpty => 'empty string',
String s => 'string "$s"',
List(length: 0) => 'empty list',
List(length: final n) => 'list of $n',
(int a, int b) => 'pair (' + a.toString() + ', ' + b.toString() + ')',
_ => 'unknown',
};
void main() {
print(area(Circle(2)));
for (final v in [null, -1, 3, 'x', <int>[], (1, 2)]) {
print(describe(v));
}
}| Pattern | Matches | Binds |
|---|---|---|
int n | Any int | n to the value |
Circle(:final radius) | A Circle | radius from the getter |
(int a, int b) | A two-element record | a and b |
[first, ...rest] | A list with at least one element | first, rest |
{'k': v} | A map containing key k | v |
final x? | A non-null value | x after the null check |
_ | Anything | Nothing: the wildcard |
case int n when n > 0 | A guard | n, only when the guard holds |
A sealed class cannot be extended outside its own library, which is what lets the compiler prove a switch is exhaustive. If you add a subclass and forget a case, the build fails instead of silently falling through.
if-case and destructuring in statements
void main() {
final json = <String, Object?>{'name': 'ada', 'age': 36};
// if-case: match a pattern and bind in one step
if (json case {'name': final String name, 'age': final int age}) {
print('$name is $age');
}
// destructuring a nullable value in a condition
String? maybe = 'text';
if (maybe case final String s when s.isNotEmpty) {
print('non-empty: $s');
}
// the classic null-check pattern replaces a cascade of null tests
final config = <String, String>{'host': 'db'};
if (config case {'host': final h, 'port': final p}) {
print('$h:$p');
} else {
print('host and port are both required');
}
// a switch statement with patterns and multiple statements per case
final value = Object();
switch (value) {
case int n when n > 100:
print('big int');
case int n:
print('int $n');
case final String s:
print('string $s');
default:
print('other');
}
// patterns work in a for-in loop body too
for (final (key, value) in {'a': 1, 'b': 2}.entries.map((e) => (e.key, e.value))) {
print('$key=$value');
}
}💡
Prefer a switch expression over a chain of
if statements when you are mapping one input to one output. It returns a value, so it can be assigned directly, and the compiler checks that every branch returns.FAQ
When should I use a record instead of a class?
For a transient grouping inside one file or one function: a computed pair, a parsed token, the result of a lookup. Promote it to a class once it needs behaviour, validation or a name that appears in an API.
Why is my switch not exhaustive?
The type is not
sealed, or the switch is over a nullable type and you have not matched null. Make the base class sealed, or add an explicit null and a wildcard case.Related
Classes, futures and streams Generics, extensions and operator overloading
Last refreshed 2026-09-18.