Dart Tutorial

Dart Lesson 60 of 102 4 min read

Patterns and Destructuring in Dart

Learn pattern matching in Dart 3: destructure lists, maps, records and objects, and use patterns in variables, if-case, for loops and switch.

On this page

A pattern describes the shape of a value. Dart can use it to do two things at once:

  • Match: check whether a value has that shape.
  • Destructure: pull the pieces out into variables.

Patterns appear in variable declarations, assignments, for loops, if, and switch.

Destructuring in a declaration #

void main() {
  var [first, second, third] = [10, 20, 30];
  print(first + second + third);

  var (name, age) = ('Asha', 28);
  print('$name is $age');

  var {'host': host, 'port': port} = {'host': 'localhost', 'port': 8080};
  print('$host:$port');
}
60
Asha is 28
localhost:8080

In a declaration the pattern must be certain to match. var [a, b] = [1, 2, 3]; throws, because the lengths differ.

List patterns and the rest element #

... matches any number of remaining items. Give it a name to capture them.

void main() {
  var numbers = [1, 2, 3, 4, 5];

  var [head, ...tail] = numbers;
  print('$head then $tail');

  var [start, ..., end] = numbers;
  print('$start ... $end');

  var [_, second, ...] = numbers; // _ ignores a value
  print(second);
}
1 then [2, 3, 4, 5]
1 ... 5
2

Object patterns #

Match an object by its class and pull out its getters.

class User {
  final String name;
  final int age;
  User(this.name, this.age);
}

void main() {
  var user = User('Bimal', 31);

  var User(name: userName, age: userAge) = user;
  print('$userName $userAge');

  // Shorthand when the variable has the same name as the getter.
  var User(:name, :age) = user;
  print('$name $age');
}
Bimal 31
Bimal 31

if-case: match, and use the parts if it fits #

When a pattern might not match, use if (value case pattern). The variables exist only inside the block.

void describe(Object value) {
  if (value case [int x, int y]) {
    print('A pair of ints: $x and $y');
  } else if (value case {'name': String name}) {
    print('A map with the name $name');
  } else if (value case String s when s.isNotEmpty) {
    print('Some text: $s');
  } else {
    print('Something else');
  }
}

void main() {
  describe([3, 4]);
  describe({'name': 'Chandra', 'age': 40});
  describe('hello');
  describe(3.14);
}
A pair of ints: 3 and 4
A map with the name Chandra
Some text: hello
Something else

Notice that a map pattern matches when the listed keys are present; extra keys are ignored. A list pattern must match the length exactly unless you use ....

Validating JSON #

Patterns make checking the structure of decoded JSON short and safe.

void main() {
  Map<String, dynamic> json = {
    'user': {'name': 'Dipa', 'roles': ['admin', 'editor']},
  };

  if (json case {'user': {'name': String name, 'roles': [String first, ...]}}) {
    print('$name, first role: $first');
  } else {
    print('Unexpected JSON');
  }
}
Dipa, first role: admin

Without patterns this would take several is checks, null checks and casts.

Patterns in for loops #

void main() {
  var prices = {'tea': 25, 'coffee': 60};
  for (final MapEntry(:key, :value) in prices.entries) {
    print('$key: $value');
  }

  var points = [(1, 2), (3, 4)];
  for (final (x, y) in points) {
    print(x * y);
  }
}
tea: 25
coffee: 60
2
12

The kinds of pattern #

PatternExampleMatches when
Constant0, 'yes', nullThe value equals the constant
Variablevar x, int xAlways (or when the type fits), and binds it
Wildcard_Always, and ignores the value
Relational> 10, <= 5, != 0The comparison is true
Logical or'y' || 'yes'Either side matches
Logical and>= 0 && < 10Both sides match
Null-checkvar x?The value is not null
Castvar x as intAlways; throws if the type is wrong
List[a, b, ...rest]It is a list of matching shape
Map{'key': v}It is a map containing those keys
Record(a, b), (:x, :y)It is a record of that shape
ObjectPoint(x: 0, :y)It is that type and its getters match

Patterns nest freely: a record inside a list inside a map is fine.

Null-check patterns #

void main() {
  String? maybeName = 'Esha';

  if (maybeName case var name?) {
    print('Hello, $name'); // name is a non-nullable String
  }

  (int?, int?) coords = (3, null);
  if (coords case (var x?, var y?)) {
    print('Both present: $x, $y');
  } else {
    print('At least one is missing');
  }
}
Hello, Esha
At least one is missing

Guards with when #

A when clause adds a condition that can use the variables the pattern just bound.

String classify((int, int) point) {
  return switch (point) {
    (0, 0) => 'origin',
    (var x, var y) when x == y => 'on the diagonal',
    (var x, _) when x < 0 => 'left side',
    _ => 'somewhere else',
  };
}

void main() {
  print(classify((0, 0)));
  print(classify((4, 4)));
  print(classify((-2, 7)));
}
origin
on the diagonal
left side

Patterns are most powerful inside switch, which is the subject of the next lesson.

Try it yourself #

Write a function that takes a List<int> and uses if-case or switch to return 'empty', 'one item: X', 'two items: X and Y', or 'starts with X, N more' for longer lists.

Practise in the playground Updated by Santosh Adhikari