Pattern Matching in Dart
By Syncfusion®, Inc. · Last updated
How to bundle values with records, and how patterns check the shape of a value and take it apart: in declarations, switch, when guards and if-case.
What Are Records?
Record = a small, fixed bundle of values, written in round brackets: ('Asha', 92).
- It can hold values of different types together.
- You do not write a class for it.
- Its size is fixed. You cannot add or remove values later.
- Its values cannot be changed after it is made.
Simple lesson: a record is a quick way to keep a few values together.
A Real-World Example
Think of a tiffin box with compartments.
- One part has rice, one has dal, one has a sweet.
- They travel together, in one box.
- You would not build a cupboard (a class) just to carry lunch.
- A record is the tiffin box:
('rice', 'dal', sweet: 'ladoo').
Every Run box on this page runs on the course's Dart runner in your browser.
Records need Dart 3 or newer. Check with dart --version on your computer.
What You Will Learn
In this lesson, you will learn:
- How to create records with positional and named fields.
- How to return several values, and how records are compared.
- Basic patterns: variables, constants, wildcards and comparisons.
- Record, object, list and map patterns.
- Patterns in switch,
whenguards, sealed classes and if-case.
Creating Records
Positional Fields
Positional field = a field known by its position: first, second, third.
A record with three positional fields
void main() {
var student = ('Asha', 13, 'Blue');
print(student);
print(student.$1);
print(student.$2);
print(student.$3);
}
- Read them with
$1,$2,$3. - Counting starts at 1 here, not 0 like lists.
Named Fields
Named field = a field with a name, written name: value.
Named fields are easier to read
void main() {
var exam = (marks: 92, subject: 'Maths');
print(exam.subject);
print(exam.marks);
print(exam);
}
Mixing Both
Click a field to see how it is read:
Positional and named together
void main() {
var r = ('Asha', 92, age: 13, house: 'Blue');
print(r.$1);
print(r.$2);
print(r.age);
print(r.house);
}
- Named fields do not count for
$1,$2. Only positional ones do. - A record with one positional field needs a comma:
('Asha',).
Records are read-only. r.$1 = 'Ravi'; is an error before running.
To "change" a record, make a new one: r = ('Ravi', r.$2);
Simple lesson: read positional fields by number with $1, and named fields by their name.
Returning Multiple Values
Without records, a function can return only one value. With records, it can return a bundle:
Lowest and highest marks in one go
(int, int) minMax(List<int> marks) {
var low = marks[0];
var high = marks[0];
for (var m in marks) {
if (m < low) low = m;
if (m > high) high = m;
}
return (low, high);
}
void main() {
var result = minMax([72, 95, 64, 88]);
print('Lowest: ${result.$1}');
print('Highest: ${result.$2}');
}
You can unpack the record straight into variables. This is called destructuring:
Unpacking a returned record
Named fields unpack by name with :name:
Unpacking named fields
({int present, int absent}) attendance(List<bool> days) {
var p = days.where((d) => d).length;
return (present: p, absent: days.length - p);
}
void main() {
var (:present, :absent) = attendance([true, true, false, true, true]);
print('Present: $present');
print('Absent: $absent');
}
You will learn every kind of unpacking below.
Simple lesson: return a record when a function has more than one answer.
Record Types and Equality
Record Types
Record type = the shape of a record: its field types, and the names of named fields.
| Record | Its type |
|---|---|
('Asha', 13) | (String, int) |
(marks: 92, subject: 'Maths') | ({int marks, String subject}) |
('Asha', age: 13) | (String, {int age}) |
Using a record type
void main() {
(String, int) topper = ('Priya', 98);
({double h, double w}) size = (h: 1.5, w: 2.5);
print(topper.runtimeType);
print(size.runtimeType);
print(size.w * size.h);
}
Equality
Two records are equal when they have the same shape and equal values:
Comparing records
void main() {
var a = ('Asha', 92);
var b = ('Asha', 92);
var c = ('Asha', 95);
print(a == b);
print(a == c);
print((x: 1, y: 2) == (y: 2, x: 1));
}
a == bistrue, even though they are two different bundles.- The order of named fields does not matter.
(x: 1, y: 2)equals(y: 2, x: 1).
Use a record for a quick bundle that a function returns or a loop uses.
Use a class when the data needs methods, rules, or a name you will use all over the app.
Easy way to remember
( , )= a record.$1, $2= positional.name:= named.- Return
(a, b), unpack withvar (a, b) = .... - Same shape + same values = equal.
Simple lesson: a record's type is its shape, and records with the same shape and values are equal.
What Is Pattern Matching?
Pattern = a description of what a value should look like. Dart compares the value with it.
Pattern matching does two jobs in one step:
- Match: does this value have the right shape? (A list of 2 items? A Student? A number above 90?)
- Destructure: if it does, take out the parts into new variables.
Simple lesson: a pattern asks "does it look like this?" and, if yes, "give me its parts".
A Real-World Example
The school office sorts the morning post.
- "If it is a parcel for a teacher, send it to the staff room."
- "If it is a letter with Exam on it, send it to the exam cell."
- "If it is anything else, keep it at the front desk."
- The clerk looks at the shape of each item, then acts. That is pattern matching.
Pick a point and see which pattern catches it:
The rest of this lesson uses records from above and sealed classes from Class Modifiers.
Destructuring
Destructuring = taking a value apart into separate variables, in one line.
Destructuring a record and a list
void main() {
var (name, marks) = ('Asha', 92);
print('$name scored $marks');
var [first, second, third] = ['Gold', 'Silver', 'Bronze'];
print('First prize: $first');
print('Third prize: $third');
}
- The left side is a pattern. Its shape must match the value.
- If the shapes do not match, Dart reports an error:
var [a, b] = [1, 2, 3];fails, because there are 3 items, not 2.
Simple lesson: put a pattern on the left of =, and Dart fills its variables from the value on the right.
Basic Patterns
Variable, Constant and Wildcard
| Pattern | Looks like | Matches | Example |
|---|---|---|---|
| Variable | var x, int x | any value (of that type), and saves it | var marks |
| Constant | 0, 'A', null, Day.monday | only that exact value | 0 |
| Wildcard | _ | any value, and throws it away | _ |
| Relational | >= 90, < 0 | values that pass the comparison | >= 90 |
Using _ to skip a part
void main() {
var (name, _, house) = ('Ravi', 14, 'Green');
print('$name is in $house house');
}
Combining with && and ||
Patterns joined with || and &&
String band(int marks) => switch (marks) {
>= 90 => 'Excellent',
>= 75 && < 90 => 'Good',
35 || 36 || 37 => 'Just passed',
>= 38 => 'Pass',
_ => 'Needs help',
};
void main() {
for (var m in [95, 80, 36, 50, 12]) {
print('$m: ${band(m)}');
}
}
||means "either pattern matches".&&means "both patterns match".
Simple lesson: small patterns (values, comparisons, _) can be joined to describe exactly what you want.
Structure Patterns
Record Patterns
Matching the shape of a point
String where((int, int) point) => switch (point) {
(0, 0) => 'At the centre',
(0, var y) => 'On the vertical line, y = $y',
(var x, 0) => 'On the horizontal line, x = $x',
(var x, var y) => 'At ($x, $y)',
};
void main() {
print(where((0, 0)));
print(where((0, 7)));
print(where((3, 0)));
print(where((2, 5)));
}
Object Patterns
Object pattern = ClassName(field: pattern). It checks the type, then reads named fields (or getters).
Matching objects by type and field
class Student {
final String name;
final int marks;
Student(this.name, this.marks);
}
void main() {
var s = Student('Priya', 96);
var Student(:name, :marks) = s;
print('$name: $marks');
var message = switch (s) {
Student(marks: >= 90, :var name) => '$name gets a gold star',
Student(:var name) => '$name, keep going',
};
print(message);
}
:nameis short forname: var name.marks: >= 90puts a relational pattern on one field.
List Patterns and Rest
Rest element = ... inside a list pattern. It matches any number of items.
First, last and the rest
void main() {
var queue = ['Asha', 'Ravi', 'Priya', 'Imran', 'Meena'];
var [first, ...others] = queue;
print('First: $first, then ${others.length} more');
var [head, ..., tail] = queue;
print('$head is first, $tail is last');
var [a, b, ...rest] = queue;
print(rest);
}
Map Patterns
Map pattern = {'key': pattern}. It checks that the key is there, then matches its value.
Pulling values out of a map
void main() {
var data = {'name': 'Kiran', 'class': 7, 'house': 'Red'};
switch (data) {
case {'name': String name, 'class': int grade}:
print('$name is in class $grade');
default:
print('Not a student record');
}
}
- A map pattern ignores keys you did not mention, like
'house'. String namealso checks the type of the value.- A key may be missing, so map patterns are used where a match can fail: in
switchorif-case(below).
Simple lesson: structure patterns match records, objects, lists and maps, and take out the parts you name.
Patterns in switch
Switch Statements and Expressions
You met switch in Conditional Statements. Every case can hold a full pattern:
A switch statement with patterns
void describe(Object value) {
switch (value) {
case int n when n < 0:
print('$n is a negative number');
case int n:
print('$n is a whole number');
case String s when s.isEmpty:
print('An empty text');
case String s:
print('Text with ${s.length} letters');
case [var a, var b]:
print('A pair: $a and $b');
default:
print('Something else');
}
}
void main() {
describe(-4);
describe(12);
describe('');
describe('Dart');
describe([1, 2]);
describe(3.5);
}
Guards with when
Guard = when condition after a pattern. The case matches only if the pattern matches and the condition is true.
Pattern first, then the guard
Exhaustiveness with Sealed Classes
With a sealed parent, Dart knows every child, so a switch can cover them all without _:
Handling every kind of API reply
sealed class Reply {}
class Success extends Reply {
final List<String> names;
Success(this.names);
}
class NotFound extends Reply {}
class Failure extends Reply {
final int code;
Failure(this.code);
}
String show(Reply r) => switch (r) {
Success(names: []) => 'No students yet',
Success(:var names) => 'Students: ${names.join(', ')}',
NotFound() => 'This class does not exist',
Failure(code: >= 500) => 'Server problem, try later',
Failure(:var code) => 'Error $code',
};
void main() {
print(show(Success(['Asha', 'Ravi'])));
print(show(Success([])));
print(show(NotFound()));
print(show(Failure(503)));
print(show(Failure(401)));
}
The first matching case wins. Put specific cases (with guards or exact values) before general ones.
If Failure(:var code) came first, Failure(code: >= 500) would never be reached. Real Dart warns about such unreachable cases.
Simple lesson: in a switch, patterns are tried from top to bottom, and when adds an extra condition.
if-case
if-case = if (value case pattern). It runs the block only if the value matches. The pattern's variables can be used inside.
Checking data before using it
void main() {
var reply = {'status': 'ok', 'marks': [88, 92, 79]};
if (reply case {'status': 'ok', 'marks': [var first, ...]}) {
print('First mark is $first');
} else {
print('Bad reply');
}
var broken = {'status': 'ok', 'marks': 'none'};
if (broken case {'marks': List<int> m}) {
print('Got ${m.length} marks');
} else {
print('marks is missing or not a list');
}
}
- One line checks the key, the type, and the list shape.
- No crash if the data is wrong. You just take the
elsepath.
Easy way to remember
var (a, b) = ...= take apart (must match).switch+ patterns = choose by shape, top to bottom.when= extra condition.if (x case ...)= check and take apart, safely.
Simple lesson: if-case checks the shape of data and gives you its parts, all in one condition.
TL;DR
- Record = a fixed bundle:
('Asha', age: 13). Read with$1and.age. - Functions can return a record. Same shape and values means
==istrue. - A pattern checks the shape of a value and takes out its parts:
var (a, b) = r; - Structure patterns:
(a, b),Student(:name),[x, ...rest],{'k': v}. - switch tries patterns top to bottom,
whenadds a guard, andif (x case ...)checks data safely.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
r.$0 for the first field | Counting starts at 0 | Record fields start at $1 | A compile error. |
r.$1 = 5; | Records can change | Records are read-only | A compile error. |
('Asha') for a one-field record | Brackets make a record | Add a comma: ('Asha',) | It is just a String in brackets. |
Counting named fields in $n | All fields are numbered | Only positional fields get $n | $3 does not exist. |
| A record for big app data | Records replace classes | Use a class with methods and rules | Hard-to-read code. |
| A general case before a specific one | Dart picks the best match | Dart picks the first match | The specific case never runs. |
var [a, b] = [1, 2, 3]; | Extra items are ignored | List patterns must match the length, or use ... | An error: the pattern does not match. |
Using case x: to save a value | A bare name saves the value | In case, write var x to save it | It compares with a constant named x. |
_ in the middle of a sealed switch | It is harmless | Cases after _ are never reached | Unreachable code, and lost exhaustiveness. |
Reading json['marks'][0] without checks | The data is always right | Use if-case to check first | A crash on bad data. |
Check Yourself
Exercises
What to Remember
- A record is a fixed, read-only bundle of values.
- Return a record when a function has more than one answer.
- A pattern checks the shape of a value and gives you its parts.
- In switch, the first match wins, and
whenadds a condition. - Use
if-caseto check outside data before you use it.
Next: Exception Handling. You will learn to catch errors and keep your program running.