Watch the video (11 min)
On this page24 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What Is an Object Pattern?
  5. 2. Matching a Type and Reading a Field
  6. 3. The : Shorthand
  7. 4. Matching on Getters
  8. 5. Testing a Field's Value
  9. 6. Type Test and Extraction in One Step
  10. 7. Nested Patterns
  11. 8. What Is a Map Pattern?
  12. 9. Extra Keys Are Ignored
  13. 10. Missing Keys, null Values and Wrong Types
  14. 11. Combining Patterns with when
  15. 12. A Real-World Example
  16. Code Examples
  17. Common Beginner Mistakes
  18. Best Practices
  19. Mini Challenge
  20. Practice Questions
  21. Practice Answers
  22. Quick Revision
  23. Key Takeaways
  24. What's Next?

Lecture 80 — Object & Map Patterns

Category13 — Patterns & Modern Dart
Lecture080 of 140
Estimated video length11 min
TargetDart 3.12.2
LevelIntermediate

List and record patterns take apart lists and records. Objects and maps need the same treatment: object patterns check a value's class and read its getters in one step, and map patterns check that a map has the right keys with the right types.

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 010, 049, 056, 065 and 077–079:

  • is type tests and as casts
  • maps, and why map[key] can return null
  • classes with final fields and getters
  • holding different subtypes in one Object or parent-typed variable
  • if (value case pattern), variable patterns, relational patterns such as > 1, and destructuring records and lists

Connection With Previous Lectures

In Lecture 065 we handled a mixed list of shapes like this:

dart
if (shape is Circle) {
  var radius = shape.radius;
  print(radius);
}

Two steps: test the type, then read the field. Maps are worse: to trust {'name': 'Ali', 'age': 20} you must check each key, that its value is not null, and its type, one if at a time. The patterns so far only take apart records and lists.


Learning Objectives

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

  • Write an object pattern such as Circle(radius: var r)
  • Use the :var radius shorthand when the variable has the getter's name
  • Match on getters, including computed ones and built-in ones like isEven
  • Test a type and extract its fields in one step
  • Nest patterns to reach values inside objects inside objects
  • Validate a map's keys and value types with a map pattern
  • Predict when a map pattern fails: missing keys, null values, wrong types
  • Combine object and map patterns with if-case and when

1. What Is an Object Pattern?

An object pattern looks like a constructor call, but it reads a value instead of building one. Circle(radius: var r) means: "Is this a Circle? If so, read its radius getter into r."

Analogy — think of a security desk. The guard first checks that you hold the right kind of pass, then reads the name printed on it. One look does both jobs.


2. Matching a Type and Reading a Field

Put the class name first, then getterName: pattern inside the parentheses:

dart
class Circle {
  final double radius;
  Circle(this.radius);
}

void main() {
  Object shape = Circle(2);
  if (shape case Circle(radius: var r)) {
    print('Radius: $r');
  }
}
output
Radius: 2.0

shape has the type Object, so shape.radius would not compile. The pattern checks that the value is a Circle, then calls radius and binds the result to the new variable r.


3. The : Shorthand

Usually the variable keeps the getter's name: radius: var radius. Start with : to write the name once:

dart
class Circle {
  final double radius;
  Circle(this.radius);
}

void main() {
  Object shape = Circle(3);
  if (shape case Circle(:var radius)) {
    print('Radius: $radius');
  }

  var Circle(:radius) = Circle(5);
  print('Declared: $radius');
}
output
Radius: 3.0
Declared: 5.0

Inside a case, write :var radius (or :final radius). In a declaration that starts with var or final, the plain form Circle(:radius) is enough, because the keyword already says "create variables". It is the same : shorthand you used for record fields.


4. Matching on Getters

An object pattern can read any getter, not just fields. A computed getter works the same way:

dart
class Rectangle {
  final double width;
  final double height;
  Rectangle(this.width, this.height);
  double get area => width * height;
}

void main() {
  Object shape = Rectangle(4, 2.5);
  if (shape case Rectangle(:var area)) {
    print('Area: $area');
  }
}
output
Area: 10.0

Built-in types have getters too. int(isEven: true) matches only even numbers, and String(:var length) reads a string's length:

dart
void main() {
  var marks = 7;
  if (marks case int(isEven: true)) {
    print('Even');
  } else {
    print('Odd');
  }
}
output
Odd

5. Testing a Field's Value

After the : goes any pattern, not only a variable. A constant or relational pattern checks the getter's value:

dart
class Student {
  final String name;
  final int marks;
  Student(this.name, this.marks);
}

void main() {
  var student = Student('Ali', 85);
  if (student case Student(marks: >= 80)) {
    print('${student.name} gets an A');
  }
  if (student case Student(name: 'Sara')) {
    print('Hello Sara');
  } else {
    print('Not Sara');
  }
}
output
Ali gets an A
Not Sara

The whole pattern matches only if the type matches and every field pattern matches.


6. Type Test and Extraction in One Step

Here is the code from the Connection rewritten with object patterns. Each if tests the type and extracts the fields together:

dart
class Circle {
  final double radius;
  Circle(this.radius);
}

class Square {
  final double side;
  Square(this.side);
}

void describe(Object shape) {
  if (shape case Circle(:var radius)) {
    print('Circle with radius $radius');
  } else if (shape case Square(:var side)) {
    print('Square with side $side');
  } else {
    print('Unknown shape');
  }
}

void main() {
  describe(Circle(1.5));
  describe(Square(4));
  describe('triangle');
}
output
Circle with radius 1.5
Square with side 4.0
Unknown shape

No is, no cast, no separate line. A value of the wrong type simply doesn't match.


7. Nested Patterns

A field pattern can be another object pattern, so you can reach several levels deep in one line:

dart
class Customer {
  final String name;
  Customer(this.name);
}

class Order {
  final Customer customer;
  final double total;
  Order(this.customer, this.total);
}

void main() {
  var order = Order(Customer('Zayan'), 1500);
  if (order case Order(customer: Customer(:var name), total: > 1000)) {
    print('$name gets free delivery');
  }
}
output
Zayan gets free delivery

Read it from the outside in: an Order whose customer is a Customer (its name goes into name) and whose total is greater than 1000.


8. What Is a Map Pattern?

A map pattern looks like a map literal with patterns as the values. It checks that each key exists and that its value matches:

dart
void main() {
  Map<String, Object?> data = {'name': 'Ali', 'age': 20};
  if (data case {'name': String name, 'age': int age}) {
    print('$name is $age');
  }
}
output
Ali is 20

String name matches only a String, so the pattern checks the key and the type and gives you a typed variable in one step: ideal for loosely typed data. Reading real JSON comes in Lectures 108–109.


9. Extra Keys Are Ignored

A map pattern only looks at the keys it names. Other keys don't stop the match:

dart
void main() {
  Map<String, Object?> data = {'name': 'Sara', 'age': 22, 'city': 'Lahore'};
  if (data case {'name': String name}) {
    print('Name: $name');
  }
}
output
Name: Sara

The match succeeds even though 'age' and 'city' are not in the pattern. (List patterns differ: [a, b] needs exactly two elements.)


10. Missing Keys, null Values and Wrong Types

A map pattern fails if a named key is missing, or if its value does not match. A null value fails a typed pattern like int age, because null is not an int:

dart
void check(Map<String, Object?> data) {
  if (data case {'name': String name, 'age': int age}) {
    print('Valid: $name, $age');
  } else {
    print('Invalid: $data');
  }
}

void main() {
  check({'name': 'Ali', 'age': 20});
  check({'name': 'Ali'});
  check({'name': 'Ali', 'age': null});
  check({'name': 'Ali', 'age': '20'});
}
output
Valid: Ali, 20
Invalid: {name: Ali}
Invalid: {name: Ali, age: null}
Invalid: {name: Ali, age: 20}

The last map looks valid when printed, but its age is the text '20'.


11. Combining Patterns with when

A when guard adds a condition that runs after the pattern matched, so it can use the new variables:

dart
void main() {
  Map<String, Object?> account = {'owner': 'Sara', 'balance': 2500.0};
  if (account case {
    'owner': String owner,
    'balance': double balance,
  } when balance >= 1000) {
    print('$owner can withdraw 1000');
  }
}
output
Sara can withdraw 1000

Map and object patterns also mix: {'student': Student(:var name)}.


12. A Real-World Example

Rows arrive as maps. Map patterns validate them into Student objects, and object patterns report on those:

dart
class Student {
  final String name;
  final int marks;
  Student(this.name, this.marks);
  bool get passed => marks >= 50;
}

void main() {
  List<Map<String, Object?>> rows = [
    {'name': 'Ali', 'marks': 82},
    {'name': 'Sara', 'marks': 45, 'class': '9A'},
    {'name': 'Zayan'},
    {'name': 'Hina', 'marks': 'ninety'},
  ];

  var students = <Student>[];
  for (var row in rows) {
    if (row case {'name': String name, 'marks': int marks}) {
      students.add(Student(name, marks));
    } else {
      print('Skipped: $row');
    }
  }

  for (var student in students) {
    if (student case Student(:var name, passed: true)) {
      print('$name passed');
    } else if (student case Student(:var name, :var marks)) {
      print('$name failed with $marks');
    }
  }
}
output
Skipped: {name: Zayan}
Skipped: {name: Hina, marks: ninety}
Ali passed
Sara failed with 45

Sara's extra 'class' key is ignored, Zayan and Hina are rejected, and the object patterns read both a field (marks) and a getter (passed).


Code Examples

Example 1 — Reading a Field

dart
class Circle {
  final double radius;
  Circle(this.radius);
}

void main() {
  Object shape = Circle(4);
  if (shape case Circle(radius: var r)) {
    print('Diameter: ${r * 2}');
  }
}
output
Diameter: 8.0

Example 2 — The Shorthand

dart
class Book {
  final String title;
  final int pages;
  Book(this.title, this.pages);
}

void main() {
  Object item = Book('Dart Basics', 240);
  if (item case Book(:var title, :var pages)) {
    print('$title has $pages pages');
  }
}
output
Dart Basics has 240 pages

Example 3 — Destructuring in a Declaration

dart
class Point {
  final int x;
  final int y;
  Point(this.x, this.y);
}

void main() {
  var Point(:x, :y) = Point(3, 7);
  print('x = $x, y = $y');
}
output
x = 3, y = 7

Example 4 — A Computed Getter

dart
class Square {
  final double side;
  Square(this.side);
  double get area => side * side;
}

void main() {
  Object shape = Square(3);
  if (shape case Square(:var area)) {
    print('Area: $area');
  }
}
output
Area: 9.0

Example 5 — Nested Objects

dart
class Address {
  final String city;
  Address(this.city);
}

class Person {
  final String name;
  final Address address;
  Person(this.name, this.address);
}

void main() {
  var person = Person('Sara', Address('Karachi'));
  if (person case Person(:var name, address: Address(city: 'Karachi'))) {
    print('$name lives in Karachi');
  }
}
output
Sara lives in Karachi

Example 6 — A Map Pattern

dart
void main() {
  Map<String, Object?> product = {
    'title': 'Notebook',
    'price': 500,
    'stock': 12,
  };
  if (product case {'title': String title, 'price': int price}) {
    print('$title costs $price');
  }
}
output
Notebook costs 500

Example 7 — Student Marks

dart
class Student {
  final String name;
  final int marks;
  Student(this.name, this.marks);
}

void main() {
  var students = [
    Student('Ali', 92),
    Student('Sara', 74),
    Student('Zayan', 38),
  ];
  for (var student in students) {
    if (student case Student(:var name, marks: >= 90)) {
      print('$name: A');
    } else if (student case Student(:var name, marks: >= 50)) {
      print('$name: Pass');
    } else {
      print('${student.name}: Fail');
    }
  }
}
output
Ali: A
Sara: Pass
Zayan: Fail

Example 8 — Shopping Total

dart
void main() {
  List<Map<String, Object?>> cart = [
    {'item': 'Pen', 'price': 50, 'qty': 4},
    {'item': 'Bag', 'price': 1500, 'qty': 1},
    {'item': 'Gift', 'price': 300},
  ];

  var total = 0;
  for (var line in cart) {
    if (line case {'price': int price, 'qty': int qty}) {
      total += price * qty;
    } else {
      print('Bad line: $line');
    }
  }
  print('Total: $total');
}
output
Bad line: {item: Gift, price: 300}
Total: 1700

Example 9 — Bank Balance

dart
class Account {
  final String owner;
  final double balance;
  Account(this.owner, this.balance);
}

void withdraw(Object account, double amount) {
  if (account case Account(:var owner, :var balance) when balance >= amount) {
    print('$owner withdraws $amount, left ${balance - amount}');
  } else {
    print('Withdrawal of $amount refused');
  }
}

void main() {
  withdraw(Account('Ali', 1000), 400);
  withdraw(Account('Sara', 200), 400);
  withdraw('not an account', 400);
}
output
Ali withdraws 400.0, left 600.0
Withdrawal of 400.0 refused
Withdrawal of 400.0 refused

Example 10 — Temperature Readings

dart
void main() {
  List<Map<String, Object?>> readings = [
    {'city': 'Lahore', 'celsius': 38.5},
    {'city': 'Murree', 'celsius': 12.0},
    {'city': 'Quetta'},
  ];

  for (var reading in readings) {
    if (reading case {'city': String city, 'celsius': double c} when c > 35) {
      print('$city: heat warning (${c * 9 / 5 + 32} F)');
    } else if (reading case {'city': String city, 'celsius': double c}) {
      print('$city: $c C');
    } else {
      print('No data: $reading');
    }
  }
}
output
Lahore: heat warning (101.3 F)
Murree: 12.0 C
No data: {city: Quetta}

Common Beginner Mistakes

Mistakes 1–3 use the Circle class from section 2.

1. Using :radius without var inside a case

dart
void main() {
  Object shape = Circle(2);
  // if (shape case Circle(:radius)) {} // ✗
}
output
Error: The getter name is not specified explicitly, and the pattern is not a variable. Try specifying the getter name explicitly, or using a variable pattern.

Inside a case, a plain name is a constant to compare against, not a new variable. Write Circle(:var radius). Only a var/final declaration allows Circle(:radius).

2. Leaving out the getter name

dart
void main() {
  Object shape = Circle(2);
  // if (shape case Circle(var r)) {} // ✗
}
output
Error: A pattern field in an object pattern must be named.
Try adding a pattern name or ':' before the pattern.

Object patterns read getters by name, not by position. Write Circle(radius: var r).

3. Naming a getter the class doesn't have

dart
void main() {
  Object shape = Circle(2);
  // if (shape case Circle(diameter: var d)) {} // ✗
}
output
Error: The getter 'diameter' isn't defined for the type 'Circle'.

The names in an object pattern are checked against the class, just like shape.diameter.

4. Using a map pattern in a declaration

dart
void main() {
  var marks = {'math': 80};
  var {'math': math, 'science': science} = marks;
  print('$math $science');
}
output
Unhandled exception:
Bad state: Pattern matching error

This compiles, but 'science' is missing, so the program crashes while running. A declaration has no "else". Check maps with if-case instead.

5. Writing an empty map pattern

dart
void main() {
  Map<String, int> marks = {'math': 80};
  // if (marks case {}) {} // ✗
}
output
Error: A map pattern must have at least one entry.
Try replacing it with an object pattern 'Map()'.

{} would match every map, so Dart forbids it. To check "is this a map?", use the object pattern Map().


Best Practices

  • Use object patterns, with the :var name shorthand, instead of is plus a field read.
  • Validate loosely typed maps with typed map patterns such as {'age': int age}.
  • Use map patterns in if-case, not in declarations, unless the keys are certain.
  • Put conditions on fields inside the pattern (marks: >= 50); use when for conditions that combine variables.

Mini Challenge

A delivery app receives orders as maps. An order is valid only if it has a String 'customer' and an int 'total'. Print Free delivery for <name> for valid orders with a total of at least 1000, Delivery fee for <name> for other valid orders, and Rejected for invalid ones.

Use these orders:

output
{'customer': 'Ali', 'total': 1200}
{'customer': 'Sara', 'total': 600, 'notes': 'Ring the bell'}
{'total': 900}

Your program should print exactly:

output
Free delivery for Ali
Delivery fee for Sara
Rejected

Try it yourself before opening the solution.

Show solution
dart
void main() {
  List<Map<String, Object?>> orders = [
    {'customer': 'Ali', 'total': 1200},
    {'customer': 'Sara', 'total': 600, 'notes': 'Ring the bell'},
    {'total': 900},
  ];

  for (var order in orders) {
    if (order case {
      'customer': String name,
      'total': int total,
    } when total >= 1000) {
      print('Free delivery for $name');
    } else if (order case {'customer': String name, 'total': int _}) {
      print('Delivery fee for $name');
    } else {
      print('Rejected');
    }
  }
}
output
Free delivery for Ali
Delivery fee for Sara
Rejected

Sara's 'notes' key is ignored; the third order has no 'customer', so both patterns fail. int _ checks the type of 'total' without creating a variable.


Practice Questions

Work through these in order — they run from beginner to application to challenge. Every question gives you exact values, so you can compare your output with the answers.

Beginner

  1. Write a class Pet with a final String name. Store Pet('Milo') in an Object variable and use Pet(name: var n) to print Pet: Milo.
  2. Rewrite question 1 with the :var shorthand.
  3. Given Map<String, Object?> user = {'name': 'Zayan', 'age': 6};, use a map pattern to print Zayan is 6.

Application

  1. Write a class Rectangle with width, height and a getter perimeter. For Rectangle(3, 4), read only the getter with an object pattern and print Perimeter: 14.0.

  2. Write a function describe(Object value) that prints Text of length N for a String and Number N for an int, and Something else otherwise. Use String(:var length). Call it with 'Sara', 42 and true.

  3. Predict, then check, which of these maps match {'id': int id}: {'id': 7}, {'id': 7, 'name': 'Ali'}, {'name': 'Ali'}, {'id': '7'}.

  4. Create Order(Customer('Ali'), 800) (classes as in section 7). Use one nested pattern to print Ali spent 800.0.

  5. This line crashes when it runs. Rewrite it with if-case so it prints No science:

    dart
    var {'math': math, 'science': science} = {'math': 80};

Challenge

  1. Given a list of maps {'name': 'Ali', 'marks': 82}, {'name': 'Sara', 'marks': 95}, {'name': 'Hina'}, print the names with marks of 90 or more, and count the invalid maps. Print Top: Sara and Invalid: 1.
  2. Write classes Circle(radius) and Square(side), each with an area getter. For the list [Circle(1), Square(2), Square(3)], use object patterns to add up the areas of the squares only and print Square area: 13.0.

Practice Answers

Question 1 — Solution

dart
class Pet {
  final String name;
  Pet(this.name);
}

void main() {
  Object pet = Pet('Milo');
  if (pet case Pet(name: var n)) {
    print('Pet: $n');
  }
}
output
Pet: Milo

Question 2 — Solution

dart
class Pet {
  final String name;
  Pet(this.name);
}

void main() {
  Object pet = Pet('Milo');
  if (pet case Pet(:var name)) {
    print('Pet: $name');
  }
}
output
Pet: Milo

Question 3 — Solution

dart
void main() {
  Map<String, Object?> user = {'name': 'Zayan', 'age': 6};
  if (user case {'name': String name, 'age': int age}) {
    print('$name is $age');
  }
}
output
Zayan is 6

Question 4 — Solution

dart
class Rectangle {
  final double width;
  final double height;
  Rectangle(this.width, this.height);
  double get perimeter => 2 * (width + height);
}

void main() {
  Object shape = Rectangle(3, 4);
  if (shape case Rectangle(:var perimeter)) {
    print('Perimeter: $perimeter');
  }
}
output
Perimeter: 14.0

Question 5 — Solution

dart
void describe(Object value) {
  if (value case String(:var length)) {
    print('Text of length $length');
  } else if (value case int n) {
    print('Number $n');
  } else {
    print('Something else');
  }
}

void main() {
  describe('Sara');
  describe(42);
  describe(true);
}
output
Text of length 4
Number 42
Something else

Question 6 — Solution

dart
void main() {
  List<Map<String, Object?>> maps = [
    {'id': 7},
    {'id': 7, 'name': 'Ali'},
    {'name': 'Ali'},
    {'id': '7'},
  ];
  for (var map in maps) {
    if (map case {'id': int id}) {
      print('$map matches, id $id');
    } else {
      print('$map does not match');
    }
  }
}
output
{id: 7} matches, id 7
{id: 7, name: Ali} matches, id 7
{name: Ali} does not match
{id: 7} does not match

Question 7 — Solution

dart
class Customer {
  final String name;
  Customer(this.name);
}

class Order {
  final Customer customer;
  final double total;
  Order(this.customer, this.total);
}

void main() {
  var order = Order(Customer('Ali'), 800);
  if (order case Order(customer: Customer(:var name), :var total)) {
    print('$name spent $total');
  }
}
output
Ali spent 800.0

Question 8 — Solution

dart
void main() {
  var marks = {'math': 80};
  if (marks case {'math': var math, 'science': var science}) {
    print('$math $science');
  } else {
    print('No science');
  }
}
output
No science

Question 9 — Solution

dart
void main() {
  List<Map<String, Object?>> rows = [
    {'name': 'Ali', 'marks': 82},
    {'name': 'Sara', 'marks': 95},
    {'name': 'Hina'},
  ];

  var invalid = 0;
  for (var row in rows) {
    if (row case {'name': String name, 'marks': int marks}) {
      if (marks >= 90) {
        print('Top: $name');
      }
    } else {
      invalid++;
    }
  }
  print('Invalid: $invalid');
}
output
Top: Sara
Invalid: 1

Question 10 — Solution

dart
class Circle {
  final double radius;
  Circle(this.radius);
  double get area => 3.14 * radius * radius;
}

class Square {
  final double side;
  Square(this.side);
  double get area => side * side;
}

void main() {
  List<Object> shapes = [Circle(1), Square(2), Square(3)];
  var total = 0.0;
  for (var shape in shapes) {
    if (shape case Square(:var area)) {
      total += area;
    }
  }
  print('Square area: $total');
}
output
Square area: 13.0

The Circle fails the Square(...) pattern, so only 4.0 + 9.0 is added.


Quick Revision

  1. What two things does Circle(radius: var r) do at once?
  2. When can you write Circle(:radius) without var?
  3. Can an object pattern read a computed getter?
  4. Does {'name': String name} match a map with an extra 'city' key?
  5. Why does {'age': int age} fail when 'age' is null?
  6. Why is a map pattern in a plain var declaration risky?
  7. What does a when guard add to an if-case?

Key Takeaways

  • An object pattern Circle(radius: var r) checks the type and reads getters in one step.
  • :var radius is shorthand for radius: var radius; plain :radius works only in var/final declarations.
  • Object patterns read any getter: fields, computed getters, and built-in ones like length and isEven.
  • Each field can hold any pattern: a variable, a constant, a relational test, or another object pattern.
  • A map pattern {'name': String name} checks that a key exists and that its value matches.
  • Map patterns ignore extra keys; a missing key, a null, or a wrong type fails the match.
  • Use them in if-case; add when for conditions that combine the new variables.

What's Next?

Lecture 81 — Pattern Matching with switch. So far we have tried our patterns one at a time with if-case and long else if chains. A switch can try many object, map, and record patterns in order and pick the first that fits. Next we will combine everything from this category with switch statements and expressions, and see how Dart checks that no case is forgotten.


Video lecture

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