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:

  1. How to create records with positional and named fields.
  2. How to return several values, and how records are compared.
  3. Basic patterns: variables, constants, wildcards and comparisons.
  4. Record, object, list and map patterns.
  5. Patterns in switch, when guards, 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.

RecordIts 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 == b is true, 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 with var (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

PatternLooks likeMatchesExample
Variablevar x, int xany value (of that type), and saves itvar marks
Constant0, 'A', null, Day.mondayonly that exact value0
Wildcard_any value, and throws it away_
Relational>= 90, < 0values 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);
}
  • :name is short for name: var name.
  • marks: >= 90 puts 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 name also checks the type of the value.
  • A key may be missing, so map patterns are used where a match can fail: in switch or if-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 else path.

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

  1. Record = a fixed bundle: ('Asha', age: 13). Read with $1 and .age.
  2. Functions can return a record. Same shape and values means == is true.
  3. A pattern checks the shape of a value and takes out its parts: var (a, b) = r;
  4. Structure patterns: (a, b), Student(:name), [x, ...rest], {'k': v}.
  5. switch tries patterns top to bottom, when adds a guard, and if (x case ...) checks data safely.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
r.$0 for the first fieldCounting starts at 0Record fields start at $1A compile error.
r.$1 = 5;Records can changeRecords are read-onlyA compile error.
('Asha') for a one-field recordBrackets make a recordAdd a comma: ('Asha',)It is just a String in brackets.
Counting named fields in $nAll fields are numberedOnly positional fields get $n$3 does not exist.
A record for big app dataRecords replace classesUse a class with methods and rulesHard-to-read code.
A general case before a specific oneDart picks the best matchDart picks the first matchThe specific case never runs.
var [a, b] = [1, 2, 3];Extra items are ignoredList patterns must match the length, or use ...An error: the pattern does not match.
Using case x: to save a valueA bare name saves the valueIn case, write var x to save itIt compares with a constant named x.
_ in the middle of a sealed switchIt is harmlessCases after _ are never reachedUnreachable code, and lost exhaustiveness.
Reading json['marks'][0] without checksThe data is always rightUse if-case to check firstA 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 when adds a condition.
  • Use if-case to check outside data before you use it.

Next: Exception Handling. You will learn to catch errors and keep your program running.