Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Dart (3.13)  Module 6 – Records and pattern matching

Every Dart pattern type, with examples

Run all fifteen Dart pattern types through one switch, learn how they group, which ones test and which throw, and how cases over records stay exhaustive.

  • Intermediate
  • 10 minutes
  • Examples run with dart 3.13.5 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Use logical, relational and parenthesized patterns, and predict how a mixed pattern is grouped
  • Choose a pattern that tests a value instead of one that throws when data may be null or mistyped
  • Distinguish a named constant in a case from a variable the case declares
  • Match lists with rest elements, maps, records and objects, and predict when each one matches
  • Check that a switch over a record of two bools covers every combination

Before you start

What you’ll learn · Before you start

What you will learn

  • Use logical, relational and parenthesized patterns, and predict how a mixed pattern is grouped
  • Choose a pattern that tests a value instead of one that throws when data may be null or mistyped
  • Distinguish a named constant in a case from a variable the case declares
  • Match lists with rest elements, maps, records and objects, and predict when each one matches
  • Check that a switch over a record of two bools covers every combination

Before you start

On this page

The Pattern types page of dart.dev lists fifteen kinds of pattern, and one case can stack several of them, as int n && >= 1000 does. Two of the fifteen throw where you might expect a quiet mismatch, one looks like a new variable but is a constant, and the way || and && group can turn a rule inside out.

You will send fourteen values through one switch with a case of each kind, then combine patterns with ||, &&, relational tests and parentheses, choose between patterns that test and patterns that throw, tell a constant from a new variable in a case, match lists, maps, records and objects, and have the compiler prove that a switch over two bools covers every combination.

Each pattern type is a test with its own job

classify.dart has one switch expression with a case of each pattern type, ordered so that every input reaches the case meant for it, and prints which case took each of fourteen inputs:

classify.dart: fourteen inputs, one case of each pattern type Dart · classify.dart
const vip = 'vip';

String classify(Object? value) => switch (value) {
  null => 'constant',
  vip => 'identifier (a constant)',
  'tea' || 'coffee' => 'logical-or',
  int n && >= 1000 => 'logical-and, relational',
  int(isNegative: true) => 'object (int getter)',
  int n => 'variable, typed',
  double d && (< 0.0 || > 1.0) => 'parenthesized',
  [_, _, ...var rest] => 'list, rest, wildcard',
  {'id': String id} => 'map',
  {'qty': var qty!} => 'null-assert',
  (String name, var count as int) => 'cast',
  (int x, int y) => 'record',
  DateTime(month: 2, day: 29) => 'object (getters)',
  final other? => 'null-check',
};

void main() {
  final inputs = <Object?>[
    null,
    'vip',
    'tea',
    2500,
    -7,
    42,
    1.5,
    [1, 2, 3, 4],
    {'id': 'u-17', 'extra': true},
    {'qty': 3},
    ('thali', 2),
    (3, 4),
    DateTime(2028, 2, 29),
    true,
  ];
  for (final v in inputs) {
    print('${classify(v).padRight(24)} <- $v');
  }
}

Output

constant                 <- null
identifier (a constant)  <- vip
logical-or               <- tea
logical-and, relational  <- 2500
object (int getter)      <- -7
variable, typed          <- 42
parenthesized            <- 1.5
list, rest, wildcard     <- [1, 2, 3, 4]
map                      <- {id: u-17, extra: true}
null-assert              <- {qty: 3}
cast                     <- (thali, 2)
record                   <- (3, 4)
object (getters)         <- 2028-02-29 00:00:00.000
null-check               <- true

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run classify.dart

Every input found its case, and the last one, true, fell through to the null-check pattern final other?, which takes any value that is not null. Each case is built from subpattern: A pattern nested inside another one, such as a field of a record pattern or an element of a list pattern. Patterns match recursively, outer pattern first, then each subpattern., smaller patterns nested inside a bigger one: [_, _, ...var rest] is a list pattern holding two wildcards and a rest element. This table lists the fifteen kinds in the page’s order, from the lowest precedence to the highest:

Pattern Written as Matches when
logical-or 'tea' || 'coffee' either branch matches, tried left to right
logical-and int n && >= 1000 both branches match
relational >= 1000 the comparison with a constant is true
cast var count as int always, after a cast that throws on a mismatch
null-check final other? the value is not null
null-assert var qty! always, after a check that throws on null
constant null, 0, const (maxSeats ~/ 2) the value equals the constant
variable int n, var x always for var; a typed one tests the type
identifier vip in a case, as a named constant
parenthesized (< 0.0 || > 1.0) the pattern inside matches
list [_, _, ...var rest] a List of a matching length and elements
map {'id': String id} a Map with those keys and matching values
record (int x, int y) a record of the same shape with matching fields
object DateTime(month: 2, day: 29) a value of that type whose getters match
wildcard _ always, binding nothing

The sections below take these kinds in groups, starting with the four that combine and compare.

Checkpoint

Check: The fifteen pattern types

  1. Question 1 of 2 In classify.dart, which case took the input 1.5?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: double d && (< 0.0 || > 1.0), the parenthesized pattern

    Cases are tried in order, and each one only takes the values its pattern describes. The parentheses keep the logical-or inside the logical-and.

  2. Question 2 of 2 Why did the input true reach the case final other? in classify.dart?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: No earlier case matched a bool, and a null-check pattern takes any value that is not null

    With null handled by the first case, a null-check pattern at the end covers every remaining value, which also makes the switch over Object? exhaustive.

Logical patterns group by precedence, and relational ones need a type

grouping.dart gives a badge for a perfect 100 or for a score from 40 to 60, first as written and then with parentheses around the logical-or, and then finds points that lie on an axis:

grouping.dart: one rule grouped two ways, and a logical-or that binds Dart · grouping.dart
// A badge for a perfect 100, or for any score from 40 to 60.
String badge(int score) => switch (score) {
  100 || >= 40 && <= 60 => 'badge',
  _ => 'none',
};

// The same parts with parentheses around the logical-or.
String regrouped(int score) => switch (score) {
  (100 || >= 40) && <= 60 => 'badge',
  _ => 'none',
};

// Both branches of a logical-or must bind the same variables.
String axis((int, int) point) => switch (point) {
  (var x, 0) || (0, var x) => 'on an axis at $x',
  _ => 'off the axes',
};

void main() {
  for (final s in [100, 50, 70]) {
    print('$s: ${badge(s)} as written, ${regrouped(s)} regrouped');
  }
  for (final p in [(5, 0), (0, -2), (1, 1)]) {
    print('$p is ${axis(p)}');
  }
}

Output

100: badge as written, none regrouped
50: badge as written, badge regrouped
70: none as written, none regrouped
(5, 0) is on an axis at 5
(0, -2) is on an axis at -2
(1, 1) is off the axes

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run grouping.dart

The two forms disagreed on 100: badge as written, none when regrouped. The Pattern types page orders the kinds by precedence, as operators are ordered: logical-or lowest, then logical-and, then relational, then the postfix cast, null-check and null-assert at one shared level, then every other pattern at the highest. A lower-precedence pattern is grouped last, so && binds before ||. Step through how the badge rule is grouped and what it does with 100 and 70:

Interactive · step by step How 100 || >= 40 && <= 60 is grouped Step through the badge pattern from grouping.dart. The tree shows how the parts are grouped; each step follows one score through it.
How 100 || >= 40 && <= 60 is grouped, step 4 of 4. Showing || (lowest), 100 (constant) (no), &&, >= 40 (yes), <= 60 (no). In focus: <= 60.|| (lowest)100 (constant)no&&>= 40yes<= 60nodecides for 100 when regrouped

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. && groups first. Logical-and has a higher precedence than logical-or, and relational patterns are higher still. So the pattern is read as 100 || (>= 40 && <= 60): a perfect score, or a score in the middle band.
  2. A score of 100. Branches of || are tried from left to right. 100 matches the constant on the left, so the right branch is never tried, and badge printed badge as written.
  3. A score of 70. 100 does not match 70, so the right branch is tried. 70 >= 40 holds, but 70 <= 60 does not, so the && fails, and with it the whole pattern: none.
  4. With parentheses. (100 || >= 40) && <= 60 groups the other way. 100 now passes the || but fails <= 60, which is why regrouped printed none for 100 and badge only for 50.

Parentheses change the grouping, which is what a parenthesized pattern is for. The axis cases show the rule for variables in a logical-or: both branches must bind the same names, because only one branch matches, and x printed 5 for (5, 0) and -2 for (0, -2). In a logical-and the opposite holds: the two sides must not bind the same name.

A relational pattern compares the value with a constant using ==, !=, <, >, <= or >=, and the value’s type must have that operator:

relational_object.dart: > 0 on a value of type Object? Dart · relational_object.dart
String sign(Object? value) => switch (value) {
  > 0 => 'positive',
  _ => 'other',
};

void main() {
  print(sign(3));
}

Output (exit status 254)

Printed as an error (standard error)

relational_object.dart:2:3: Error: The method '>' isn't defined for the type 'Object?'.
 - 'Object' is from 'dart:core'.
Try correcting the name to the name of an existing method, or defining a method named '>'.
  > 0 => 'positive',
  ^

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run relational_object.dart

“The method ‘>’ isn’t defined for the type ‘Object?’”, and exit status 254. In classify.dart, int n && >= 1000 compiled because the int test stands on the left: on its right, the value is known to be an int.

Key idea

&& groups before ||, and relational tests before both. When a rule mixes them, add parentheses until the grouping you mean is the only one possible.

Checkpoint

Check: Logical and relational patterns, and precedence

  1. Question 1 of 2 How does Dart group the pattern 100 || >= 40 && <= 60?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 100 || (>= 40 && <= 60)

    The page lists logical-or as the lowest precedence and logical-and above it, as with || and && in expressions. As written, 100 earned a badge; regrouped, it did not.

  2. Question 2 of 2 case > 0 on a value of type Object? does not compile. Which case does, and why?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: case int n && > 0, because the int test on the left makes the value an int on the right

    relational_object.dart failed with "The method '>' isn't defined for the type 'Object?'". classify.dart's int n && >= 1000 compiled because of the type test before it.

Some patterns test a value, others throw on it

Four of the fifteen look alike in pairs: a typed variable and a cast both talk about a type, and a null-check and a null-assert both talk about null. test_or_throw.dart runs each pair on one value that fits and one that does not:

test_or_throw.dart: a type test against a cast, ? against ! Dart · test_or_throw.dart
String typeTest(Object? v) => switch (v) {
  int n => 'matched $n',
  _ => 'no match',
};

String cast(Object? v) => switch (v) {
  final n as int => 'gave $n',
};

String nullCheck(int? v) => switch (v) {
  final n? => 'matched $n',
  _ => 'no match',
};

String nullAssert(int? v) => switch (v) {
  final n! => 'gave $n',
};

String attempt(String Function() f) {
  try {
    return f();
  } on TypeError catch (e) {
    return 'threw a TypeError: $e';
  }
}

void main() {
  for (final v in <Object?>[31, 'x']) {
    print('$v  int n: ${typeTest(v)}  |  as int: ${attempt(() => cast(v))}');
  }
  for (final v in <int?>[31, null]) {
    print('$v  n?: ${nullCheck(v)}  |  n!: ${attempt(() => nullAssert(v))}');
  }
}

Output

31  int n: matched 31  |  as int: gave 31
x  int n: no match  |  as int: threw a TypeError: type 'String' is not a subtype of type 'int' in type cast
31  n?: matched 31  |  n!: gave 31
null  n?: no match  |  n!: threw a TypeError: Null check operator used on a null value

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run test_or_throw.dart

On 31 all four gave the value. On 'x', int n reported no match while as int, a cast pattern: A pattern written with as and a type, such as var n as int. It casts the value before matching the inner pattern and throws when the value does not have the type., threw a TypeError, “type ‘String’ is not a subtype of type ‘int’ in type cast”. On null, the null-check pattern: A pattern written with a trailing question mark, such as final n?. It matches only a value that is not null, binds it with the non-nullable type, and treats null as a mismatch. n? reported no match, and the null-assert pattern: A pattern written with a trailing exclamation mark, such as var n!. It lets values that are not null through with the non-nullable type and throws when the value is null. n! threw “Null check operator used on a null value”. The table puts the four side by side:

Pattern On a value that fits On one that does not
int n, a typed variable binds n as an int no match, the next case is tried
final n as int, a cast binds n as an int throws a TypeError
final n?, null-check binds n with a non-nullable type no match on null
final n!, null-assert binds n with a non-nullable type throws a TypeError on null

The throwing two are for places where a mismatch is a bug, not a possibility: the page’s example is var (x!, y!) = position;, a declaration that must have two non-null values. In a case that sorts input, use the testing two, so that a value of the wrong kind moves on to the next case.

A cast where a type test was meant

case final n as int: reads like “if it is an int”, but it is “make it an int or crash”, and every later case becomes unreachable. Write case int n: to test, and keep as for values that are guaranteed to have the type.

Checkpoint

Check: Patterns that test and patterns that throw

  1. Question 1 of 2 What does a switch with the single case final n as int do with the value 'x'?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It throws a TypeError, because the cast fails

    as int threw "type 'String' is not a subtype of type 'int' in type cast". Use a typed variable pattern when the value may have another type.

  2. Question 2 of 2 In case final n? on an int? value, what is the type of n inside the case?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: int

    A null-check pattern tests for null and then binds the value with its non-nullable type, so no ! is needed in the body.

A name in a case is a constant, not a new variable

constants.dart sorts seat counts with a named constant, a constant expression and a literal, and binds everything else:

constants.dart: constant patterns and a binding case Dart · constants.dart
const maxSeats = 120;

String seats(int n) => switch (n) {
  maxSeats => 'full house',
  const (maxSeats ~/ 2) => 'half full',
  0 => 'empty',
  var other => 'other: $other',
};

void main() {
  for (final n in [120, 60, 0, 75]) {
    print('$n: ${seats(n)}');
  }
}

Output

120: full house
60: half full
0: empty
75: other: 75

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run constants.dart

120 matched maxSeats, 60 matched const (maxSeats ~/ 2), 0 the literal, and 75 reached var other. A A pattern that matches when the value is equal to a constant: a literal, a named constant, a const constructor or a const collection. can be a literal, a named constant or a const constructor; a longer constant expression must be wrapped as const ( ... ), as maxSeats ~/ 2 was. The surprise is the bare name. The page says an identifier in a matching context, such as a case, is treated as a named constant, so case maxSeats: compares and never declares. A name that is not a constant fails:

local_name.dart: a final local variable used as a case Dart · local_name.dart
void main() {
  final limit = 3;
  switch (3) {
    case limit:
      print('at the limit');
  }
}

Output (exit status 254)

Printed as an error (standard error)

local_name.dart:4:10: Error: Not a constant expression.
    case limit:
         ^^^^^

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run local_name.dart

The compiler said “Not a constant expression”: limit is a final variable, not a const one, and a case cannot declare it either. The same identifier means different things by place, as the page sets out:

Where the identifier stands What it does
a pattern declaration, var (a, b) = ... declares a new variable
a pattern assignment, (a, b) = ... assigns to an existing variable
a case compares with a named constant
_, anywhere matches anything and binds nothing

To bind a new variable inside a case, write var, final or a type in front of the name, as var other did.

Checkpoint

Check: Constants and identifiers in cases

  1. Question 1 of 2 const maxSeats = 120; In switch (n) { case maxSeats: ... }, what does case maxSeats do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It matches when n == 120

    The page treats an identifier in a matching context as a constant pattern. That is why a non-constant name, like the final local in local_name.dart, is refused.

  2. Question 2 of 2 Which case binds every remaining value to a new variable called other?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: case var other:

    To declare inside a case, write var, final or a type in front of the name. Without it, the name is read as a constant.

Lists, maps and records match in different ways

collections.dart describes API responses, lists and records with one switch; the first two inputs are maps with a key the patterns do not mention:

collections.dart: nine inputs against map, list and record patterns Dart · collections.dart
String describe(Object? json) => switch (json) {
  {'status': 'ok', 'data': [var first, ...]} => 'ok, first item $first',
  {'status': 'error', 'code': int code} => 'error $code',
  [] => 'an empty list',
  [var only] => 'a list of one: $only',
  [var a, var b] => 'a list of two: $a, $b',
  (var a, var b) => 'a record of two: $a, $b',
  (:var name) => 'a record with only a name: $name',
  _ => 'no case matched',
};

void main() {
  final inputs = <Object?>[
    {
      'status': 'ok',
      'data': [7, 8],
      'took_ms': 12,
    },
    {'status': 'error', 'code': 404, 'hint': 'check the id'},
    [],
    [5],
    [5, 6],
    [5, 6, 7],
    (1, 2),
    (name: 'Asha'),
    (name: 'Asha', age: 31),
  ];
  for (final v in inputs) {
    print(describe(v));
  }
}

Output

ok, first item 7
error 404
an empty list
a list of one: 5
a list of two: 5, 6
no case matched
a record of two: 1, 2
a record with only a name: Asha
no case matched

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run collections.dart

The two response maps matched although each carried an extra key, ‘took_ms’ and ‘hint’. The page states the rule: a map pattern ignores keys the pattern does not name, and every key it does name must be present. Lists are stricter. A list pattern matches only a list of exactly its length, so [5, 6, 7] fell past [var a, var b] to the last case, unless the pattern has a rest element: The ... element of a list pattern, which matches any number of elements, none included, so the list need not have the pattern's exact length. Written ...rest, it collects them into a new list.: [var first, ...] matched a data list of any length from one up, and a list pattern may hold only one rest element. Records are the strictest: a record pattern needs exactly the record’s shape, so (name: 'Asha', age: 31) did not match (:var name), which describes a record with a single named field.

Pattern Extra elements or fields Too few
list, no rest element no match no match
list with a rest element collected by the rest no match
map extra keys are ignored a named key that is missing: no match
record no match no match

One response pattern, many versions of the API

Because extra keys are ignored, a map pattern written for one version of a JSON response keeps matching after the server adds fields. A list or a record pattern would stop matching instead.

Checkpoint

Check: List, map and record patterns

  1. Question 1 of 2 Does the list [5, 6, 7] match the pattern [var a, var b]?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No, a list pattern without a rest element needs exactly its length

    List patterns match the exact length. Add a rest element, such as [var a, ...], to accept longer lists.

  2. Question 2 of 2 Does {'status': 'error', 'code': 404, 'hint': 'check the id'} match {'status': 'error', 'code': int code}?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Yes, map patterns ignore keys they do not name

    A map pattern looks only at the keys it names, which is why a pattern written for one API response keeps working when fields are added.

Object patterns check a type and read its getters

leap_days.dart labels five dates with object patterns on DateTime, one of them a date that does not exist:

leap_days.dart: object patterns on DateTime values Dart · leap_days.dart
bool isLeapDay(DateTime d) => switch (d) {
  DateTime(month: 2, day: 29) => true,
  _ => false,
};

String note(DateTime d) => switch (d) {
  DateTime(month: 2, day: 29, :final year) => 'leap day of $year',
  DateTime(month: 1, day: 1) => 'new year',
  DateTime(weekday: >= DateTime.saturday) => 'weekend',
  _ => 'an ordinary day',
};

void main() {
  final days = [
    DateTime(2028, 2, 29),
    DateTime(2025, 2, 29),
    DateTime(2026, 1, 1),
    DateTime(2031, 5, 10),
    DateTime(2031, 5, 12),
  ];
  for (final d in days) {
    print(
      '${d.toIso8601String().substring(0, 10)}: ${note(d)}, leap day: ${isLeapDay(d)}',
    );
  }
}

Output

2028-02-29: leap day of 2028, leap day: true
2025-03-01: weekend, leap day: false
2026-01-01: new year, leap day: false
2031-05-10: weekend, leap day: false
2031-05-12: an ordinary day, leap day: false

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run leap_days.dart

An object pattern: A pattern that names a type and some of its getters, such as DateTime(month: 2). It matches a value of that type whose named getters match their subpatterns, ignoring the rest. such as DateTime(month: 2, day: 29, :final year) names a type and some of its getters, each with a subpattern. Step through what it checks on the two dates written with month 2 and day 29:

Interactive · step by step What an object pattern checks on a DateTime Step through the first case of note in leap_days.dart. The left column is the pattern's parts, the right one what the two dates gave.
What an object pattern checks on a DateTime, step 4 of 4. Showing is it a DateTime?, month: 2, day: 29, :final year, DateTime(2028, 2, 29) (month 2, day 29), DateTime(2025, 2, 29) (month 3, day 1). In focus: DateTime(2025, 2, 29), month: 2.is it a DateTime?month: 2day: 29:final yearDateTime(2028, 2, 29)month 2, day 29DateTime(2025, 2, 29)month 3, day 1

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. First, the type. An object pattern names a type. The value must have that type before any getter is read; for a value of another type the case simply does not match.
  2. Then each getter. month: 2 calls the month getter and matches the result against the constant 2; day: 29 does the same with day. Getters the pattern does not name, such as hour, are not looked at.
  3. A real leap day. The date built from 2028, 2 and 29 has month 2 and day 29, so the pattern matches, and :final year binds 2028, the year getter's value. The case printed leap day of 2028.
  4. A date that rolled over. 2025 has no leap day, and the DateTime constructor moved day 29 of month 2 into March, as the printout showed. Its month getter gives 3, so the pattern fails at month: 2.

The 2028 date matched and bound its year. The 2025 one printed as the first day of March, because the DateTime constructor carried the extra day over, so its month getter gave 3 and the leap-day case did not match; it became a weekend instead. weekday: >= DateTime.saturday put a relational pattern inside the object pattern, against the constant DateTime.saturday. As with records, :final year is shorthand that reads the getter year into a variable of the same name. Unlike a record pattern, an object pattern does not need to mention every property: the page says an object with extra fields still matches.

Checkpoint

Check: Object patterns and getters

  1. Question 1 of 2 What does DateTime(month: 2, day: 29) check first when it is matched against a value?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: That the value is a DateTime

    An object pattern first tests the type, then matches each named getter against its subpattern.

  2. Question 2 of 2 Why did DateTime(2025, 2, 29) not match the leap-day case in leap_days.dart?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The constructor carried the day over into March, so its month getter gave 3

    Object patterns read the object as it is. A date with no leap day is normalised by the constructor, and its getters report the date it became.

A switch over a record of bools must cover all four cases

A shop’s product page has two facts to show, whether the item is in stock and whether the shop is open. exhaustive.dart switches on both at once, as a record of two bools:

exhaustive.dart: one case for each of the four combinations Dart · exhaustive.dart
String label(bool inStock, bool open) => switch ((inStock, open)) {
  (true, true) => 'order now',
  (true, false) => 'order for tomorrow',
  (false, true) => 'notify me',
  (false, false) => 'closed',
};

void main() {
  for (final inStock in [true, false]) {
    for (final open in [true, false]) {
      print('in stock $inStock, open $open: ${label(inStock, open)}');
    }
  }
}

Output

in stock true, open true: order now
in stock true, open false: order for tomorrow
in stock false, open true: notify me
in stock false, open false: closed

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run exhaustive.dart

Each of the four combinations printed its own label:

open closed
in stock order now order for tomorrow
out of stock notify me closed

A switch expression must be exhaustiveness: The compiler's check that a switch expression's cases cover every possible value of the matched type; for a bool, enum or sealed type a missing case is a compile-time error. More in “Dart switch statements and expressions”, and the compiler checks that across combinations, not only for one bool. missing_case.dart drops the last case:

missing_case.dart: the same switch without (false, false) Dart · missing_case.dart
String label(bool inStock, bool open) => switch ((inStock, open)) {
  (true, true) => 'order now',
  (true, false) => 'order for tomorrow',
  (false, true) => 'notify me',
};

void main() {
  print(label(false, false));
}

Output (exit status 254)

Printed as an error (standard error)

missing_case.dart:1:50: Error: The type '(bool, bool)' is not exhaustively matched by the switch cases since it doesn't match '(false, false)'.
Try adding a wildcard pattern or cases that match '(false, false)'.
String label(bool inStock, bool open) => switch ((inStock, open)) {
                                                 ^

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run missing_case.dart

The compiler named the missing combination: the type ‘(bool, bool)’ “is not exhaustively matched by the switch cases since it doesn’t match ‘(false, false)’”. The switch lesson of the control-flow module shows the same check for a single bool, enums and sealed classes (switch statements and expressions); records extend it to every combination of their fields.

Checkpoint

Check: Exhaustive switches over records

  1. One question A switch expression on (inStock, open) has cases for (true, true), (true, false) and (false, true). What happens?

    Choose one answer.

    Show the answer: One question

    Answer: It does not compile, and the error names (false, false)

    missing_case.dart failed with "doesn't match '(false, false)'". Records let the exhaustiveness check cover combinations of fields.

Practise: two pattern drills

The drills package has two functions to write with patterns, and six sample tests that pin down what each returns:

The drills package: two stubs and six sample tests

practice/pubspec.yaml

name: drills
description: Two pattern drills with six sample tests.
publish_to: none

environment:
  sdk: ^3.13.0

dev_dependencies:
  test: ^1.33.0

lib/drills.dart · practice/lib/drills.dart

/// Describes a shape decoded from JSON: a map with 'type' 'circle' and a
/// number 'r', or 'type' 'rect' with numbers 'w' and 'h'. Anything else is
/// 'unknown'.
String shapeOf(Object? json) {
  return 'not written yet';
}

/// Reads a date written as year-month-day digits separated by '-', or
/// returns null when the text has another form or a part is not a number.
({int year, int month, int day})? parseDate(String text) {
  return null;
}

test/drills_test.dart · practice/test/drills_test.dart

import 'package:drills/drills.dart';
import 'package:test/test.dart';

void main() {
  test('a circle with a radius', () {
    expect(shapeOf({'type': 'circle', 'r': 2}), 'circle of radius 2');
  });

  test('a rectangle with two sides', () {
    expect(shapeOf({'type': 'rect', 'w': 3, 'h': 4.5}), 'rectangle 3 by 4.5');
  });

  test('a circle without its radius is unknown', () {
    expect(shapeOf({'type': 'circle'}), 'unknown');
  });

  test('a date with three parts', () {
    expect(parseDate('2031-05-10'), (year: 2031, month: 5, day: 10));
  });

  test('a date with two parts is refused', () {
    expect(parseDate('2031-05'), isNull);
  });

  test('a date with letters is refused', () {
    expect(parseDate('20x1-05-10'), isNull);
  });
}

The starter’s shapeOf answers every input with the same text, and its parseDate always returns null, so four of the six sample tests fail; the two that pass already are the ones that expect null:

The drills as handed out and their sample tests Python · practice_session.py
from session import files, run

# The two drills as handed out: shapeOf gives every input the same answer, and parseDate always returns null.
files("practice=drills")
run(
    """
cd drills
dart pub get --offline > /dev/null
!dart test --reporter failures-only
""",
    pub=True,
)

Output

$ cd drills
$ dart pub get --offline > /dev/null
$ dart test --reporter failures-only
+0 -1: test/drills_test.dart: a circle with a radius [E]
  Expected: 'circle of radius 2'
    Actual: 'not written yet'
     Which: is different.
            Expected: circle of  ...
              Actual: not writte ...
                      ^
             Differ at offset 0

  package:matcher            expect
  test/drills_test.dart 6:5  main.<fn>

+0 -2: test/drills_test.dart: a rectangle with two sides [E]
  Expected: 'rectangle 3 by 4.5'
    Actual: 'not written yet'
     Which: is different.
            Expected: rectangle  ...
              Actual: not writte ...
                      ^
             Differ at offset 0

  package:matcher             expect
  test/drills_test.dart 10:5  main.<fn>

+0 -3: test/drills_test.dart: a circle without its radius is unknown [E]
  Expected: 'unknown'
    Actual: 'not written yet'
     Which: is different.
            Expected: unknown
              Actual: not writte ...
                      ^
             Differ at offset 0

  package:matcher             expect
  test/drills_test.dart 14:5  main.<fn>

+0 -4: test/drills_test.dart: a date with three parts [E]
  Expected: ({int day, int month, int year}):<(day: 10, month: 5, year: 2031)>
    Actual: <null>

  package:matcher             expect
  test/drills_test.dart 18:5  main.<fn>

+2 -4: Some tests failed.

Consider enabling the flag chain-stack-traces to receive more detailed exceptions.
For example, 'dart test --chain-stack-traces'.
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 practice_session.py

  1. Put pubspec.yaml, lib/drills.dart and test/drills_test.dart in a new directory of your own; fetch dependencies with dart pub get, then run dart test and watch four tests fail.
  2. Write shapeOf as a switch expression with one map pattern per shape, a typed subpattern for each number, and a last case for everything else.
  3. Write parseDate: split the text at ‘-’, turn each part into an int or null, and match the result with one list pattern of three typed elements.
  4. Run dart test until “All tests passed!”, then add tests of your own: a shape with an extra key, a radius given as text, and a date with four parts.

After the drills, the self-check asks about each section of this page:

Practice that adapts to you

Practise this lesson

14 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 14 Which lists does [_, _, ...var rest] match?

    Choose one answer.

    Show the answer: Question 1 of 14

    Answer: Lists of two or more elements

    Wildcards hold places without binding, and the rest element collects whatever follows, so [1, 2, 3, 4] gave rest = [3, 4].

  2. Question 2 of 14 In classify.dart, the int 2500 also fits the later case int n. Which case printed, and why?

    Choose one answer.

    Show the answer: Question 2 of 14

    Answer: The logical-and case, because cases are tried in order and the first match wins

    A switch tries its cases from the top and stops at the first match, so specific cases go before general ones.

  3. Question 3 of 14 Why does (var x, 0) || (0, var x) compile while (var x, 0) || (0, var y) would not?

    Choose one answer.

    Show the answer: Question 3 of 14

    Answer: Both branches of a logical-or must bind the same variables, because only one of them matches

    The page sets the rule: the branches of a logical-or define the same variables, while the two sides of a logical-and must not overlap.

  4. Question 4 of 14 In which order are the branches of a logical-or pattern tried?

    Choose one answer.

    Show the answer: Question 4 of 14

    Answer: Left to right, stopping at the first branch that matches

    For 100 in grouping.dart, the constant on the left matched and the band check was never tried.

  5. Question 5 of 14 Where does a null-assert pattern such as var (x!, y!) = position; make sense?

    Choose one answer.

    Show the answer: Question 5 of 14

    Answer: Where null would be a bug, so failing loudly is right

    The throwing patterns state an assumption and crash when it is false. Input from outside the program needs the testing patterns.

  6. Question 6 of 14 What does case int n do with the value 'x'?

    Choose one answer.

    Show the answer: Question 6 of 14

    Answer: It does not match, and the next case is tried

    int n means "if it is an int, call it n". Only as int insists on the type.

  7. Question 7 of 14 How must the case for half of maxSeats be written?

    Choose one answer.

    Show the answer: Question 7 of 14

    Answer: case const (maxSeats ~/ 2):

    Literals and named constants can stand alone; other constant expressions need the const ( ... ) wrapper, as in constants.dart.

  8. Question 8 of 14 Where does a bare identifier such as a declare or assign a variable?

    Choose every answer that is right.

    Show the answer: Question 8 of 14

    Answer:

    • In a pattern declaration such as var (a, b) = pair;
    • In a pattern assignment such as (a, b) = pair;

    The identifier pattern changes meaning with its place: it declares, assigns or compares. Only _ means the same everywhere.

  9. Question 9 of 14 How many rest elements may one list pattern contain?

    Choose one answer.

    Show the answer: Question 9 of 14

    Answer: One

    One rest element per list pattern keeps the match unambiguous: the fixed elements are counted from each end.

  10. Question 10 of 14 Does (name: 'Asha', age: 31) match the pattern (:var name)?

    Choose one answer.

    Show the answer: Question 10 of 14

    Answer: No, a record pattern needs the record's exact shape

    Record patterns match the whole shape. To accept the larger record, name the age field too, for example (:var name, age: _).

  11. Question 11 of 14 Does DateTime(month: 1, day: 1) also check the hour of the date?

    Choose one answer.

    Show the answer: Question 11 of 14

    Answer: No, an object pattern reads only the getters it names

    An object pattern checks the type and the getters it lists. The page notes that an object with extra fields still matches.

  12. Question 12 of 14 What does DateTime(weekday: >= DateTime.saturday) match?

    Choose one answer.

    Show the answer: Question 12 of 14

    Answer: Saturdays and Sundays

    Subpatterns of an object pattern can be any pattern, including relational ones against a constant, as leap_days.dart labelled the weekend dates.

  13. Question 13 of 14 Why switch on (inStock, open) instead of nesting two if statements?

    Choose one answer.

    Show the answer: Question 13 of 14

    Answer: Every combination gets a case, and the compiler reports one that is missing

    The record makes the pair one value, and the exhaustiveness check turns a forgotten combination into a compile error.

  14. Question 14 of 14 How many cases does a switch over a record of three bools need when every case names one combination?

    Type a number.

    Show the answer: Question 14 of 14

    Answer: 8 cases

    Each bool doubles the combinations: 2 times 2 times 2 is 8, as two bools gave 4 in exhaustive.dart.

Interview questions

Warm-up (2-5 years): What separates the null-check pattern from the null-assert pattern, and when does each fit?

Both patterns match a value only when it is not null and then give the inner pattern a non-nullable type, so a variable bound through either has the base type without the question mark. They differ on null. The null-check pattern, written with a trailing question mark, treats null as an ordinary mismatch: the case does not match and the next one is tried, which suits switches and if-case statements that sort input that may be missing. The null-assert pattern, written with a trailing exclamation mark, throws when the value is null, a TypeError on the VM with the message Null check operator used on a null value. It fits places where null would be a bug, such as a declaration that destructures a record whose fields are nullable by type but always set by that point. For input from outside the program, prefer the null-check form.

Two longer questions in the bank start where this section ends:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A checkout handler reads an optional coupon with case {'items': List items, 'coupon': String? code}. Orders from the web send "coupon": null and work. Orders from the mobile app leave the key out, and every one of them falls to the malformed-order case. Why, and how would you match both kinds of order?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Senior to staff

    A switch over a Cart has five cases that each test the getter total with an object pattern, and total adds up every line item. A teammate wants to compute total into a variable before the switch so that it runs once. Is that needed, and what does Dart promise about getters read during a switch?

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. How many kinds of pattern does Dart have, and how do cases use them?

    Fifteen. Constants and identifiers compare, variables bind, and list, map, record and object patterns take values apart through subpatterns; one case can stack several, as int n && >= 1000 does.

  2. How does 100 || >= 40 && <= 60 group?

    As 100 || (>= 40 && <= 60): logical-or has the lowest precedence, then logical-and, then relational patterns. When a rule mixes them, add parentheses.

  3. How do the null-check pattern (x?) and the null-assert pattern (x!) differ?

    Both match only a non-null value and give the inner pattern a non-nullable type. On null, the null-check pattern simply does not match, so the next case is tried; the null-assert pattern throws. Prefer the null-check form for input from outside the program.

  4. Why is case final n as int: not a type test?

    A cast pattern means "make it an int or crash": it throws on another type, and every later case becomes unreachable. Write case int n: to test, and keep as for values guaranteed to have the type.

  5. In a case, is a bare name such as limit a new variable?

    No, a bare identifier in a case is a named constant. Write var, final or a type to bind a new variable, and wrap longer constant expressions in const ( ... ).

  6. Which of list, map and record patterns tolerate extra data?

    Map patterns ignore extra keys, so they keep matching when an API adds fields. List and record patterns need the exact length or shape, unless a list pattern has a rest element.

  7. What does an object pattern check?

    The value's type, and then only the getters it names, each against its subpattern.

  8. What must a switch over a record of two bools cover?

    All four combinations; the compiler checks that the cases cover every one.

Key takeaways

  • Dart has fifteen kinds of pattern, and every case is built from them: constants and identifiers compare, variables bind, and list, map, record and object patterns take values apart through subpatterns.
  • Logical-or has the lowest precedence, then logical-and, then relational patterns, so 100 || >= 40 && <= 60 groups as 100 || (>= 40 && <= 60), and a relational pattern needs a value whose type has the operator.
  • A typed variable and a null-check pattern test and move on, while a cast and a null-assert pattern throw a TypeError on a mismatch, so only the testing pair belongs in cases that sort input.
  • A bare identifier in a case is a named constant, never a new variable; write var, final or a type to bind, and wrap longer constant expressions in const ( ... ).
  • List and record patterns need the exact length or shape unless a list has a rest element, map patterns ignore extra keys, object patterns check the type and read only the getters they name, and the compiler checks that a switch over a record of bools covers every combination.

Terms in this lesson

cast pattern
A pattern written with as and a type, such as var n as int. It casts the value before matching the inner pattern and throws when the value does not have the type.
null-assert pattern
A pattern written with a trailing exclamation mark, such as var n!. It lets values that are not null through with the non-nullable type and throws when the value is null.
null-check pattern
A pattern written with a trailing question mark, such as final n?. It matches only a value that is not null, binds it with the non-nullable type, and treats null as a mismatch.
object pattern
A pattern that names a type and some of its getters, such as DateTime(month: 2). It matches a value of that type whose named getters match their subpatterns, ignoring the rest.
rest element
The ... element of a list pattern, which matches any number of elements, none included, so the list need not have the pattern's exact length. Written ...rest, it collects them into a new list.
subpattern
A pattern nested inside another one, such as a field of a record pattern or an element of a list pattern. Patterns match recursively, outer pattern first, then each subpattern.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.