Watch the video (12 min)
On this page28 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What Is a Pattern Type?
  5. 2. Where We Try Them
  6. 3. Constant Patterns
  7. 4. Variable Patterns
  8. 5. The Wildcard Pattern
  9. 6. Relational Patterns
  10. 7. Logical-Or Patterns: ||
  11. 8. Logical-And Patterns: &&
  12. 9. Parenthesized Patterns
  13. 10. Cast Patterns: as
  14. 11. Null-Check Patterns: ?
  15. 12. Null-Assert Patterns: !
  16. 13. List Patterns
  17. 14. Record Patterns
  18. 15. Nesting Patterns
  19. 16. A Real-World Example
  20. Code Examples
  21. Common Beginner Mistakes
  22. Best Practices
  23. Mini Challenge
  24. Practice Questions
  25. Practice Answers
  26. Quick Revision
  27. Key Takeaways
  28. What's Next?

Lecture 79 — Pattern Types

Category13 — Patterns & Modern Dart
Lecture079 of 140
Estimated video length12 min
TargetDart 3.12.2
LevelIntermediate

You have already used _, >= 90 and 'Sat' || 'Sun' without knowing they had names. Each one is a pattern type, and Dart has a dozen of them that click together like building blocks.

Contents

Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways

Every error message in this lecture is real. Each one was produced by running the code shown on Dart 3.12.2, so you will recognize it when you meet it yourself.


Prerequisites

Students should already understand, from Lectures 013–015, 024–025, 076–078:

  • nullable types, and the ! operator (Lectures 013–015)
  • switch statements and switch expressions, including _, ||, >= and when (Lectures 024–025)
  • records such as ('Ali', 85) and (name: 'Sara', marks: 91) (Lecture 076)
  • what a pattern is, matching with if (value case pattern) (Lecture 077)
  • destructuring with var (name, marks) = record; (Lecture 078)

Connection With Previous Lectures

In Lecture 025 we wrote this, and it worked:

dart
void main() {
  var marks = 82;
  var grade = switch (marks) {
    >= 90 => 'A',
    >= 80 => 'B',
    _ => 'C',
  };
  print(grade);
}
output
B

But what is >= 90? What else can go on the left of =>? Can a case check that a value is not null, or that a list has exactly three items, and pull them out? Lectures 077–078 showed that patterns match and destructure. This lecture tours every pattern type, one at a time.


Learning Objectives

By the end of this lecture you will be able to:

  • Name the main pattern types and say what each one checks
  • Use constant, variable, and wildcard patterns
  • Match ranges with relational patterns and combine patterns with || and &&
  • Group patterns with parentheses
  • Handle nullable values with the null-check ? and null-assert ! patterns
  • Convert a value with a cast pattern, and know when it throws
  • Match lists and records by shape, including the rest element ...
  • Nest patterns inside each other to check a whole structure at once

1. What Is a Pattern Type?

A pattern describes a shape a value might have. Each pattern type is one kind of description. Here is the whole set for this lecture:

Pattern typeExampleMatches when…
Constant5, 'red', nullthe value equals the constant
Variablevar m, int nalways (if the type fits), and binds a name
Wildcard_, int _always (if the type fits), binds nothing
Relational>= 90, != 0the comparison is true
Logical-orp1 || p2either side matches
Logical-andp1 && p2both sides match
Parenthesized(p1 || p2)the inner pattern matches
Castvar n as intalways — but it throws if the cast fails
Null-checkvar n?the value is not null
Null-assertvar n!always — but it throws on null
List[a, b, ...]the value is a list of that shape
Record(a, b), (name: n)the value is a record of that shape

Two more, object and map patterns, get their own lecture next.


2. Where We Try Them

Any pattern works after case in a switch, and after case in an if:

dart
void main() {
  var marks = 95;
  if (marks case >= 90) {
    print('Excellent');
  }
}
output
Excellent

Read it as "if marks matches the pattern >= 90".


3. Constant Patterns

A constant pattern matches when the value is equal to a constant: a number, a string, true, false, null, or a const variable.

dart
void main() {
  const passMark = 50;
  var marks = 50;
  switch (marks) {
    case 100:
      print('Perfect');
    case passMark:
      print('Just passed');
    default:
      print('Other');
  }
}
output
Just passed

These are the plain case values from Lecture 024. A non-const variable is not allowed here (see Mistake 1).


4. Variable Patterns

A variable pattern declares a new variable and puts the matched value in it. With a type, like int n, it also checks the type first.

dart
void main() {
  Object value = 42;
  switch (value) {
    case int n:
      print('A whole number: $n');
    case String s:
      print('Some text: $s');
    default:
      print('Something else');
  }
}
output
A whole number: 42

Inside the first case n is an int, so n + 1 would work. var n and final n also bind, without checking a type.


5. The Wildcard Pattern

The wildcard _ matches anything and binds nothing. A typed wildcard, int _, checks the type and still binds nothing:

dart
void main() {
  Object value = 3.5;
  var kind = switch (value) {
    int _ => 'int',
    double _ => 'double',
    _ => 'other',
  };
  print(kind);
}
output
double

Use int _ when you care about the type but not the value.


6. Relational Patterns

A relational pattern is a comparison operator (==, !=, <, <=, >, >=) followed by a constant. You met it in Lecture 025:

dart
void main() {
  var temperature = -3;
  if (temperature case < 0) {
    print('Freezing');
  }
}
output
Freezing

The value being matched is the left side of the comparison: temperature < 0.


7. Logical-Or Patterns: ||

p1 || p2 matches if either pattern matches. Dart tries the left side first.

dart
void main() {
  var day = 'Sun';
  var kind = switch (day) {
    'Sat' || 'Sun' => 'Weekend',
    _ => 'Weekday',
  };
  print(kind);
}
output
Weekend

If the sides declare variables, both sides must declare the same ones (Mistake 4).


8. Logical-And Patterns: &&

p1 && p2 matches only if both patterns match. It is how you write a range:

dart
void main() {
  var marks = 67;
  if (marks case >= 50 && < 75) {
    print('Pass');
  }
}
output
Pass

It can also mix kinds: int n && > 0 checks the type, binds n, and checks it is positive.


9. Parenthesized Patterns

&& binds more tightly than ||, just like with bool values in Lecture 021. Parentheses change the grouping:

dart
void main() {
  var marks = 101;
  var label = switch (marks) {
    (< 0 || > 100) => 'Invalid',
    >= 50 => 'Pass',
    _ => 'Fail',
  };
  print(label);
}
output
Invalid

Here they only add clarity; in int _ && (< 0 || > 100) they change the meaning.


10. Cast Patterns: as

A cast pattern var n as int tells Dart "treat this value as an int". It does not check first: if the value is not an int, the program crashes.

dart
void main() {
  Object amount = 250;
  if (amount case var n as int) {
    print(n * 2);
  }
}
output
500

Use it only when you are certain. Otherwise use int n, which simply fails to match.


11. Null-Check Patterns: ?

A null-check pattern var b? matches only when the value is not null, and the new variable has the non-nullable type:

dart
void showBonus(int? bonus) {
  if (bonus case var b?) {
    print('Bonus: ${b + 5}');
  } else {
    print('No bonus');
  }
}

void main() {
  showBonus(15);
  showBonus(null);
}
output
Bonus: 20
No bonus

b is an int, not an int?, so b + 5 compiles. The value comes in as a parameter so that Dart cannot know in advance whether it is null.


12. Null-Assert Patterns: !

A null-assert pattern var b! also gives a non-nullable variable, but on null it throws, exactly like the ! operator from Lecture 015:

dart
void showStock(int? stock) {
  switch (stock) {
    case var s!:
      print('In stock: $s');
  }
}

void main() {
  showStock(8);
}
output
In stock: 8

Rule of thumb: ? means "skip if null"; ! means "I promise it's not null".


13. List Patterns

A list pattern [p1, p2, …] matches a list with exactly that many elements, and matches each element with its own pattern. A rest element ... matches any number of elements:

dart
void main() {
  var marks = [90, 75, 82, 60];
  switch (marks) {
    case []:
      print('No marks');
    case [var only]:
      print('One mark: $only');
    case [var first, ..., var last]:
      print('First $first, last $last');
  }
}
output
First 90, last 60

[var first, ..., var last] means "two or more elements; bind the first and last". ...var middle would keep the skipped part as a list.


14. Record Patterns

A record pattern matches a record with the same shape. Named fields are written name: pattern; :name is short for name: name.

dart
void main() {
  var result = (name: 'Sara', marks: 91);
  switch (result) {
    case (name: 'Ali', :var marks):
      print('Ali scored $marks');
    case (:var name, marks: >= 90):
      print('$name got an A');
    default:
      print('Keep going');
  }
}
output
Sara got an A

The second case checks the field marks with a relational pattern and binds name.


15. Nesting Patterns

Every pattern type can sit inside a list or record pattern. That is where they get powerful:

dart
void main() {
  var order = ('Zayan', [1200, 450]);
  if (order case (String who, [int a, int b]) when a + b > 1000) {
    print('$who spent ${a + b}');
  }
}
output
Zayan spent 1650

One line checks the record shape, the types, the list length, and a when guard. Guards and switch exhaustiveness get a full lecture in Lecture 081.


16. A Real-World Example

Sara's school receives each result as a record of a name and a nullable list of marks:

dart
void main() {
  var results = [
    ('Ali', [80, 75, 85]),
    ('Sara', <int>[]),
    ('Zayan', null),
    ('Hina', [101]),
  ];

  for (var r in results) {
    var report = switch (r) {
      (var name, null) => '$name: absent',
      (var name, []) => '$name: no marks yet',
      (var name, [(< 0 || > 100)]) => '$name: invalid mark',
      (var name, [var m]) => '$name: one mark, $m',
      (var name, [var first, ...var rest]) =>
        '$name: $first and ${rest.length} more',
    };
    print(report);
  }
}
output
Ali: 80 and 2 more
Sara: no marks yet
Zayan: absent
Hina: invalid mark

Eight pattern types work together, and Dart confirms the arms cover every case.


Code Examples

Example 1 — Constant Patterns

dart
void main() {
  var light = 'red';
  switch (light) {
    case 'green':
      print('Go');
    case 'red':
      print('Stop');
    default:
      print('Wait');
  }
}
output
Stop

Example 2 — Typed Variable Patterns

dart
void main() {
  Object item = 'Notebook';
  var text = switch (item) {
    int n => 'Number $n',
    String s => 'Text of length ${s.length}',
    _ => 'Unknown',
  };
  print(text);
}
output
Text of length 8

Example 3 — Relational and Logical-And

dart
void main() {
  var age = 15;
  var ticket = switch (age) {
    < 5 => 'Free',
    >= 5 && < 18 => 'Child',
    _ => 'Adult',
  };
  print(ticket);
}
output
Child

Example 4 — Logical-Or

dart
void main() {
  var month = 12;
  if (month case 12 || 1 || 2) {
    print('Winter');
  }
}
output
Winter

Example 5 — Null-Check vs Null-Assert

dart
void greet(String? nickname) {
  if (nickname case var n?) {
    print('Hi, $n');
  } else {
    print('No nickname');
  }
}

void showCity(String? city) {
  switch (city) {
    case var c!:
      print('City: $c');
  }
}

void main() {
  greet(null);
  showCity('Lahore');
}
output
No nickname
City: Lahore

Example 6 — Cast Pattern

dart
void main() {
  Object price = 99.5;
  if (price case var p as double) {
    print(p.round());
  }
}
output
100

Example 7 — Student Marks with a List Pattern

dart
void main() {
  var marks = [88, 92, 79];
  if (marks case [var a, var b, var c]) {
    print('Ali total: ${a + b + c}');
  }
}
output
Ali total: 259

The pattern needs exactly three elements; with four marks, nothing would print.

Example 8 — Shopping Total with Rest

dart
void main() {
  var prices = [1200, 450, 300, 50];
  if (prices case [var first, ...var others]) {
    var total = first;
    for (var p in others) {
      total += p;
    }
    print('Items: ${others.length + 1}, total: $total');
  }
}
output
Items: 4, total: 2000

first is 1200, and ...var others collects the rest as the list [450, 300, 50].

Example 9 — Bank Balance with a Record Pattern

dart
void main() {
  var account = (owner: 'Sara', balance: -200);
  var message = switch (account) {
    (:var owner, balance: < 0) => '$owner is overdrawn',
    (:var owner, balance: 0) => '$owner has nothing left',
    (:var owner, :var balance) => '$owner has $balance',
  };
  print(message);
}
output
Sara is overdrawn

How it works

  1. :var owner binds the field owner to a variable with the same name.
  2. balance: < 0 checks the field with a relational pattern; -200 matches.
  3. The last arm binds both fields, so it catches every other record.

Example 10 — Temperature Readings

dart
void main() {
  var readings = [('Mon', 31.5), ('Tue', null), ('Wed', -2.0)];
  for (var r in readings) {
    var text = switch (r) {
      (var day, null) => '$day: sensor offline',
      (var day, var t? && < 0) => '$day: $t, freezing',
      (var day, var t?) => '$day: $t',
    };
    print(text);
  }
}
output
Mon: 31.5
Tue: sensor offline
Wed: -2.0, freezing

How it works

  1. null is a constant pattern for the offline case.
  2. var t? && < 0 checks "not null" and "below zero" on the same field.
  3. The last arm catches every remaining non-null reading.

Common Beginner Mistakes

1. Using a normal variable as a constant pattern

dart
void main() {
  var passMark = 50;
  var marks = 50;
  switch (marks) {
    case passMark:
      print('Just passed');
    default:
      print('Other');
  }
}
output
Error: Not a constant expression.
    case passMark:
         ^^^^^^^^

A name after case must be a constant. Declare it const passMark = 50;.

2. A null-assert pattern on a null value

dart
void main() {
  int? bonus;
  switch (bonus) {
    case var b!:
      print('Bonus: $b');
  }
}
output
Unhandled exception:
Null check operator used on a null value

It compiles but crashes. Use var b? when null is possible.

3. A cast pattern on the wrong type

dart
void main() {
  Object value = 'Ali';
  switch (value) {
    case var n as int:
      print(n + 1);
  }
}
output
Unhandled exception:
type 'String' is not a subtype of type 'int' in type cast

as never fails to match; it throws. Use int n to test the type instead.

4. Different variables on the two sides of ||

dart
void main() {
  var point = (0, 5);
  switch (point) {
    case (var x, 0) || (0, var y):
      print('On an axis');
    default:
      print('Elsewhere');
  }
}
output
Error: Variable pattern 'y' is missing in this branch of the logical-or pattern.
Try declaring this variable pattern in the branch.
    case (var x, 0) || (0, var y):
         ^

Both sides must bind the same names, such as (var n, 0) || (0, var n).

5. Forgetting that a list pattern checks the length

[var a, var b] does not match [80, 75, 90]. Add ... when the length can vary.


Best Practices

  • Prefer int n (test) over var n as int (crash) unless the type is guaranteed.
  • Prefer var x? over var x!; save ! for values that truly cannot be null.
  • Put the most specific arms first: [] and [var one] before [var first, ...].
  • Use _ or int _ for parts you do not need, instead of inventing unused names.
  • Add parentheses whenever you mix || and &&, even when they are not required.

Mini Challenge

Write a program that describes a cart, stored as a List<int> of prices. Print Empty cart for [], One item: P for a single price, and N items, first P otherwise. Test it with [], [500] and [300, 200, 100].

output
Empty cart
One item: 500
3 items, first 300

Try it yourself before opening the solution.

Show solution
dart
void main() {
  var carts = [
    <int>[],
    [500],
    [300, 200, 100],
  ];
  for (var cart in carts) {
    var text = switch (cart) {
      [] => 'Empty cart',
      [var p] => 'One item: $p',
      [var first, ...] => '${cart.length} items, first $first',
    };
    print(text);
  }
}
output
Empty cart
One item: 500
3 items, first 300

[] matches zero elements, [var p] exactly one, and [var first, ...] one or more, so the three arms cover every list.


Practice Questions

Work through these in order — they run from beginner to application to challenge.

Beginner

  1. Store var answer = 'yes'; and use a switch expression with constant patterns to print Agreed for 'yes', Refused for 'no', and Unclear otherwise.
  2. Store Object value = true; and use typed variable patterns to print bool: true, int: … or other.
  3. Store var speed = 72; and use if-case with a relational pattern to print Too fast when speed > 60.

Application

  1. Store var marks = 45; and use a switch expression with && patterns: 0–49 gives Fail, 50–100 gives Pass, anything else Invalid.
  2. Write void pay(int? discount) that uses a null-check pattern to print Price: 90 for a price of 100 and a discount of 10, or No discount for null. Call it with both.
  3. Store var scores = [7, 9, 4, 8]; and use a list pattern with a rest element to print First: 7, Last: 8.
  4. Store var student = ('Ali', 88); and use a record pattern to print Ali passed when the mark is >= 50, or Ali failed otherwise.

Challenge

  1. Store var day = 'Tue'; and var hour = 20;. Use a switch on the record (day, hour) with logical-or and relational patterns to print Weekend for 'Sat' or 'Sun', Evening for a weekday hour >= 18, and Working hours otherwise.
  2. Store var accounts = [(owner: 'Sara', balance: 500), (owner: 'Zayan', balance: null)]; Loop over it and print Sara: 500 or Zayan: closed, using null and ? patterns.
  3. Store var data = [3, 'Ali', 2.5, [1, 2]]; (a List<Object>). Loop over it and print small int for an int below 10, name Ali for a String, pair 1 2 for a list of exactly two elements, and other for anything else.

Practice Answers

Question 1 — Solution

dart
void main() {
  var answer = 'yes';
  var text = switch (answer) {
    'yes' => 'Agreed',
    'no' => 'Refused',
    _ => 'Unclear',
  };
  print(text);
}
output
Agreed

Question 2 — Solution

dart
void main() {
  Object value = true;
  var text = switch (value) {
    bool b => 'bool: $b',
    int n => 'int: $n',
    _ => 'other',
  };
  print(text);
}
output
bool: true

Question 3 — Solution

dart
void main() {
  var speed = 72;
  if (speed case > 60) {
    print('Too fast');
  }
}
output
Too fast

Question 4 — Solution

dart
void main() {
  var marks = 45;
  var result = switch (marks) {
    >= 0 && < 50 => 'Fail',
    >= 50 && <= 100 => 'Pass',
    _ => 'Invalid',
  };
  print(result);
}
output
Fail

Question 5 — Solution

dart
void pay(int? discount) {
  var price = 100;
  if (discount case var d?) {
    print('Price: ${price - d}');
  } else {
    print('No discount');
  }
}

void main() {
  pay(10);
  pay(null);
}
output
Price: 90
No discount

Question 6 — Solution

dart
void main() {
  var scores = [7, 9, 4, 8];
  if (scores case [var first, ..., var last]) {
    print('First: $first, Last: $last');
  }
}
output
First: 7, Last: 8

Question 7 — Solution

dart
void main() {
  var student = ('Ali', 88);
  var text = switch (student) {
    (var name, >= 50) => '$name passed',
    (var name, _) => '$name failed',
  };
  print(text);
}
output
Ali passed

Question 8 — Solution

dart
void main() {
  var day = 'Tue';
  var hour = 20;
  var text = switch ((day, hour)) {
    ('Sat' || 'Sun', _) => 'Weekend',
    (_, >= 18) => 'Evening',
    _ => 'Working hours',
  };
  print(text);
}
output
Evening

Question 9 — Solution

dart
void main() {
  var accounts = [
    (owner: 'Sara', balance: 500),
    (owner: 'Zayan', balance: null),
  ];
  for (var a in accounts) {
    var text = switch (a) {
      (:var owner, balance: null) => '$owner: closed',
      (:var owner, balance: var b?) => '$owner: $b',
    };
    print(text);
  }
}
output
Sara: 500
Zayan: closed

Question 10 — Solution

dart
void main() {
  var data = <Object>[
    3,
    'Ali',
    2.5,
    [1, 2],
  ];
  for (var item in data) {
    var text = switch (item) {
      int _ && < 10 => 'small int',
      String s => 'name $s',
      [var a, var b] => 'pair $a $b',
      _ => 'other',
    };
    print(text);
  }
}
output
small int
name Ali
other
pair 1 2

2.5 is a double, so only the wildcard matches it.


Quick Revision

  1. What is the difference between a constant pattern and a variable pattern?
  2. What does int _ check, and what does it bind?
  3. Write a pattern that matches numbers from 10 up to, but not including, 20.
  4. Which binds more tightly in a pattern: || or &&?
  5. What happens when a cast pattern's type is wrong?
  6. How is var x? different from var x!?
  7. What does ... mean inside a list pattern?
  8. What is :var marks short for in a record pattern?

Key Takeaways

  • A pattern type is one kind of shape description; patterns nest inside each other.
  • Constant patterns compare with ==; variable patterns bind a name and can test a type (int n); the wildcard _ matches anything and binds nothing.
  • Relational patterns compare with <, >=, !=…; || and && combine patterns, and parentheses group them.
  • Null-check ? skips null; null-assert ! and cast as throw when wrong.
  • List patterns check the length unless they contain a rest element ....
  • Record patterns match positional and named fields; :name binds a field by its name.

What's Next?

Lecture 80 — Object & Map Patterns. Every pattern today looked at numbers, strings, lists and records. Real programs are full of objects — a Student with a name and marks, a Map read from a file. Next we will match objects by their getters with Student(:var name) and maps by their keys with {'name': var n}, using all the pattern types from this lecture inside them.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.