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.
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:
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
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:
// 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:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- && 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.
- 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.
- 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.
- 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:
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
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:
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
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:
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:
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
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:
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
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:
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:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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
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:
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:
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
Practise: two pattern drills
The drills package has two functions to write with patterns, and six sample tests that pin down what each returns:
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:
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
- Put
pubspec.yaml,lib/drills.dartandtest/drills_test.dartin a new directory of your own; fetch dependencies withdart pub get, then rundart testand watch four tests fail. - Write
shapeOfas a switch expression with one map pattern per shape, a typed subpattern for each number, and a last case for everything else. - 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. - Run
dart testuntil “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.
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.
-
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?
-
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?
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
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.
-
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.
-
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.
-
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.
-
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 ( ... ).
-
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.
-
What does an object pattern check?
The value's type, and then only the getters it names, each against its subpattern.
-
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 && <= 60groups as100 || (>= 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,finalor a type to bind, and wrap longer constant expressions inconst ( ... ). - 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
- Pattern types (The Dart project (Google))
- Patterns (The Dart project (Google))
- Patterns feature specification (The Dart project (Google))
- DateTime class (The Dart project (Google))
Report a problem with this lesson
Kept only in this browser. Your Learn progress