skip to content

Destructuring & Matching

Dart 3 added records for anonymous multi-value data, patterns that test and destructure values, and switch expressions checked for exhaustiveness. Interviewers use them to test modern Dart fluency.

part ofDartoverview, primer and where to startread it →
on this pageshow

explore

questions

13

In Dart 3, how do you return both the minimum and maximum temperature from one function, and how does the caller read them?

level: juniorimportance: must knowfreq 62%

answer

  1. no class, no List, no Map
  2. a record type as the return type
  3. named fields read like getters
  4. positional fields are $1, $2
  5. Dart 3.0 language feature

basics

~20 s

Declare a record return type such as ({double min, double max}) and return (min: lo, max: hi). The caller reads range.min and range.max, or $1 and $2 for positional fields, with each field keeping its static type.

solid answer

~40 s

Since Dart 3.0 a function can return a **record**: an anonymous, immutable, fixed-size bundle of typed fields. Write the return type as `({double min, double max})` and return `(min: lo, max: hi)`. The caller gets one value and reads `range.min` and `range.max`, both statically typed `double`. With positional fields, `(double, double)`, the getters are `$1` and `$2` instead, which is fine for a private helper but less readable at a distance, so named fields suit anything public. Records replace the old workarounds: a `List<double>` loses which element is which and gives no per-position types, a `Map<String, Object?>` loses the types entirely, and a dedicated class is a whole declaration for two numbers. The caller can also destructure the result into two locals with a pattern.

code

dart · 18 lines
dart
({double min, double max}) temperatureRange(List<double> readings) {
  if (readings.isEmpty) {
    throw ArgumentError.value(readings, 'readings', 'must not be empty');
  }
  var min = readings.first;
  var max = readings.first;
  for (final r in readings.skip(1)) {
    if (r < min) min = r;
    if (r > max) max = r;
  }
  return (min: min, max: max);
}

void main() {
  final range = temperatureRange([12.5, 9.5, 17.25]);
  print('${range.min} to ${range.max}'); // 9.5 to 17.25
  print(range.max - range.min); // 7.75
}

go deeper

for a junior

Be able to write a function that returns ({double min, double max}) and read the two values with .min and .max, or $1 and $2 for positional fields.

for a middle

Explain why a record beats a List or Map for multiple returns: fixed shape, per-field static types, and no declaration, and when named fields beat positional ones.

for a senior

Choose records for local, private multiple returns and recognise when a returned bundle has grown into something that deserves a named type or a class.

for a principal

Set conventions for where records may appear in public APIs of shared packages, since their structural types make every same-shaped value interchangeable.

## The problem records solve A Dart function has exactly one return value. Before Dart 3.0, returning two things — here the lowest and highest reading from a list of temperatures — meant choosing a workaround: | Approach | What you lose | |---|---| | `List<double>` of two items | which index means what; a wrong length is not a compile error | | `Map<String, Object?>` | every value's static type; typos in keys compile | | a small class `TempRange` | nothing, but it is a full declaration for two numbers | | callbacks or mutable holders | readability, and they fight the language | A **record** is the language-level answer: an **anonymous, immutable, aggregate** value that bundles a fixed number of **typed fields** without declaring a class. ## Writing the function ```dart ({double min, double max}) temperatureRange(List<double> readings) { if (readings.isEmpty) { throw ArgumentError.value(readings, 'readings', 'must not be empty'); } var min = readings.first; var max = readings.first; for (final r in readings.skip(1)) { if (r < min) min = r; if (r > max) max = r; } return (min: min, max: max); } ``` Three pieces of syntax matter: - The **record type** `({double min, double max})` is the return type. Named fields sit inside curly braces within the parentheses, just like named parameters. - The **record expression** `(min: min, max: max)` builds the value, like named arguments in a call. - Record fields are **never optional**: every field in the type must be supplied, and there is no `required` keyword because none is needed. ## Reading the result The record exposes one **getter per field**: ```dart final range = temperatureRange([12.5, 9.5, 17.25]); print('${range.min} to ${range.max}'); // 9.5 to 17.25 ``` Each getter keeps its own static type, so `range.min.toStringAsFixed(1)` compiles with no cast. There are **no setters**: a record's fields cannot be reassigned. If you choose **positional** fields instead: ```dart (double, double) minMax(List<double> readings) { final sorted = [...readings]..sort(); return (sorted.first, sorted.last); } final r = minMax([12.5, 9.5, 17.25]); print(r.$1); // 9.5 print(r.$2); // 17.25 ``` Positional getters are named `$1`, `$2` and so on. They are fine for a short private helper, but at a call site several screens away `r.$1` says nothing about meaning — and a `(double, double)` accepts any two doubles in any order, so swapping them in the `return` compiles. Named fields make the type itself say `min` and `max`. The caller can also pull both fields into local variables in one declaration with a **record pattern**, `final (:min, :max) = temperatureRange(readings);` — destructuring is its own topic, but it is the idiom you will see most often next to multiple returns. ## Where it fits in a Flutter app - A widget's helper that computes a chart's axis range returns `({double min, double max})` and the `build` method reads the two fields. - A repository method returns `(List<Reading> items, bool hasMore)` for one page of results. - A list of simple same-shaped rows, such as button definitions with a label, an icon and a callback, can be a `List` of records without any class declaration. ## Common mistakes - **Returning a `List` and documenting the order.** The compiler cannot check that index 0 is the minimum; a record field named `min` can be checked. - **Swapping positional fields.** `return (hi, lo);` for a `(double, double)` compiles; named fields make the mistake visible. - **Expecting `[0]`-style access.** Records are not collections: there is no index operator, no `length` and no iteration over fields — only the getters. - **Trying to mutate the result.** There are no setters; build a new record with the changed field instead. ## Things to keep in mind 1. Records require **language version 3.0** or later; a package's default language version is the lower bound of its SDK constraint, so a constraint starting at 3.0 or above enables them. 2. A record is a **value**: you can store it in variables, lists, maps and sets, pass it to functions and nest records inside records. 3. When the same record type appears in several signatures, give it a name with a **typedef** so a change happens in one place. 4. When the bundle starts needing behaviour or validation, that is the signal to move to a class.

  • Why prefer `({double min, double max})` over `(double, double)` for a public function?
    With positional fields the caller reads `$1` and `$2`, which carry no meaning, and any two doubles in either order satisfy the type, so swapping them compiles silently. Named fields put `min` and `max` into the type itself, the call site reads `range.min`, and returning `(min: hi, max: lo)` at least looks wrong in review.
  • Can you add a field to a record value after it is created, or reassign one?
    No. A record has a fixed shape and exposes only getters, so there is no way to add, remove or reassign a field. To change one, build a new record, for example `(min: range.min, max: newMax)`.

saying these in an interview costs you the question

  • Returns a List<double> and documents which index is the minimum
  • Thinks a record field can be reassigned like a class field
  • Believes records need a class or typedef declared first
  • Says record fields lose their types and must be cast
  • Claims positional record fields are read with [0] and [1]
open as a page

In Dart 3, how does a switch expression differ from a switch statement, and where may a switch expression appear?

level: juniorimportance: must knowfreq 62%

basics

~20 s

A Dart 3 switch expression yields a value: cases are pattern => expression, comma-separated, _ is the only default, and it must be exhaustive. A switch statement runs statements and is exhaustiveness-checked only on types like enums, bool and sealed classes.

open as a page

In Dart 3, how would you validate and destructure a decoded weather-API JSON map with if-case, and what does each part of the pattern check?

level: middleimportance: must knowfreq 55%

basics

~20 s

Write if (json case {'name': String city, 'main': {'temp': num temp}}). It checks that json is a map with those keys and value types, binds city and temp only on a full match, and otherwise runs the else branch.

open as a page

In Dart 3, how would you model a PaymentResult as a sealed class hierarchy and handle every outcome with an exhaustive switch?

level: seniorimportance: must knowfreq 55%

basics

~20 s

Declare sealed class PaymentResult with one subclass per outcome in the same library, each holding its own data, then switch over it with one object pattern per subtype and no wildcard. The compiler proves coverage and flags every switch when an outcome is added.

open as a page

In Dart 3, how do you pull the fields of a returned record or an object's getters into local variables with a pattern?

level: juniorimportance: should knowfreq 50%

basics

~20 s

Write a pattern variable declaration: var (city, temp) = latestReading(); for a positional record, final (:city, :temp) = ... for named fields, and var Forecast(:high, :low) = forecast; to read an object's getters into new locals.

open as a page

In Dart, what is the difference between refutable and irrefutable pattern contexts, and why does a bare `case limit:` not bind a new variable?

level: middleimportance: should knowfreq 36%

basics

~20 s

Declarations and assignments are irrefutable: their patterns must match or throw. if-case and switch cases are refutable: a mismatch just fails. In a case, a bare identifier is a constant pattern, so case limit: compares instead of binding.

open as a page

In Dart 3, how do relational patterns like `>= 0 && < 15` and logical-or patterns work, and what rules limit their operands and bindings?

level: middleimportance: should knowfreq 30%

basics

~20 s

A relational pattern such as < 0 compares the value with a constant; && needs both sides to match, || needs one. Operands must be constants, || branches must bind the same variables, and && sides must not share a name.

open as a page

In Dart, when are two records equal, and why can `(1, [2]) == (1, [2])` still be false?

level: middleimportance: should knowfreq 42%

basics

~20 s

Records get == and hashCode automatically: equal when they have the same shape and every field is equal by that field's own ==. A List field compares by identity, so two records holding different but identical-looking lists are not equal.

open as a page

In Dart, when are two record types the same type, and how do positional field names, named field names and field order affect it?

level: middleimportance: should knowfreq 38%

basics

~20 s

Record types are structural: the same shape and field types make the same type. Positional field names are documentation only, named field names are part of the type, and the order of named fields does not matter.

open as a page

In Dart 3, which switches must be exhaustive, and why does a switch on a bool? with only true and false cases fail to compile?

level: middleimportance: should knowfreq 46%

basics

~20 s

Every Dart 3 switch expression must be exhaustive, as must a switch statement over bool, an enum, a sealed class or a nullable form of one. A bool? switch with only true and false leaves null unmatched, a compile error.

open as a page

In a Dart 3 switch, what happens when a case's when guard evaluates to false, and how do guarded cases affect exhaustiveness?

level: middleimportance: should knowfreq 38%

basics

~20 s

A when guard is checked after its pattern matches and binds variables; if it is false, matching continues with the next case instead of leaving the switch. Guarded cases never count toward exhaustiveness, so an unguarded case must still cover that value.

open as a page

In Dart patterns, when would you use `var t?`, `var t!` or `t as double` on a reading whose temperature may be null, and what happens on a bad value?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Use the null-check pattern var t? when null is expected: null fails the match. Use null-assert var t! when null is a bug: it throws. A cast pattern t as double insists on a type and throws on a wrong one.

open as a page

When should a Dart API use a record, or a record typedef, instead of a class, and what signals that it is time to switch?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Use a record for small, data-only bundles with a local or private reach: multiple returns, composite keys, lists of simple rows. Switch to a class when the data needs invariants, behaviour, encapsulation, or a distinct type that same-shaped values cannot impersonate.

open as a page