On this page24 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. What Is an Object Pattern?
- 2. Matching a Type and Reading a Field
- 3. The : Shorthand
- 4. Matching on Getters
- 5. Testing a Field's Value
- 6. Type Test and Extraction in One Step
- 7. Nested Patterns
- 8. What Is a Map Pattern?
- 9. Extra Keys Are Ignored
- 10. Missing Keys, null Values and Wrong Types
- 11. Combining Patterns with when
- 12. A Real-World Example
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 80 — Object & Map Patterns
| Category | 13 — Patterns & Modern Dart |
| Lecture | 080 of 140 |
| Estimated video length | 11 min |
| Target | Dart 3.12.2 |
| Level | Intermediate |
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:
istype tests andascasts- maps, and why
map[key]can returnnull - classes with
finalfields and getters - holding different subtypes in one
Objector 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:
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 radiusshorthand 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,
nullvalues, wrong types - Combine object and map patterns with
if-caseandwhen
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:
class Circle {
final double radius;
Circle(this.radius);
}
void main() {
Object shape = Circle(2);
if (shape case Circle(radius: var r)) {
print('Radius: $r');
}
}Radius: 2.0shape 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:
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');
}Radius: 3.0
Declared: 5.0Inside 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:
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');
}
}Area: 10.0Built-in types have getters too. int(isEven: true) matches only even numbers, and
String(:var length) reads a string's length:
void main() {
var marks = 7;
if (marks case int(isEven: true)) {
print('Even');
} else {
print('Odd');
}
}Odd5. Testing a Field's Value
After the : goes any pattern, not only a variable. A constant or relational pattern
checks the getter's value:
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');
}
}Ali gets an A
Not SaraThe 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:
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');
}Circle with radius 1.5
Square with side 4.0
Unknown shapeNo 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:
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');
}
}Zayan gets free deliveryRead 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:
void main() {
Map<String, Object?> data = {'name': 'Ali', 'age': 20};
if (data case {'name': String name, 'age': int age}) {
print('$name is $age');
}
}Ali is 20String 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:
void main() {
Map<String, Object?> data = {'name': 'Sara', 'age': 22, 'city': 'Lahore'};
if (data case {'name': String name}) {
print('Name: $name');
}
}Name: SaraThe 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:
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'});
}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:
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');
}
}Sara can withdraw 1000Map 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:
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');
}
}
}Skipped: {name: Zayan}
Skipped: {name: Hina, marks: ninety}
Ali passed
Sara failed with 45Sara'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
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}');
}
}Diameter: 8.0Example 2 — The Shorthand
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');
}
}Dart Basics has 240 pagesExample 3 — Destructuring in a Declaration
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');
}x = 3, y = 7Example 4 — A Computed Getter
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');
}
}Area: 9.0Example 5 — Nested Objects
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');
}
}Sara lives in KarachiExample 6 — A Map Pattern
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');
}
}Notebook costs 500Example 7 — Student Marks
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');
}
}
}Ali: A
Sara: Pass
Zayan: FailExample 8 — Shopping Total
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');
}Bad line: {item: Gift, price: 300}
Total: 1700Example 9 — Bank Balance
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);
}Ali withdraws 400.0, left 600.0
Withdrawal of 400.0 refused
Withdrawal of 400.0 refusedExample 10 — Temperature Readings
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');
}
}
}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
void main() {
Object shape = Circle(2);
// if (shape case Circle(:radius)) {} // ✗
}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
void main() {
Object shape = Circle(2);
// if (shape case Circle(var r)) {} // ✗
}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
void main() {
Object shape = Circle(2);
// if (shape case Circle(diameter: var d)) {} // ✗
}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
void main() {
var marks = {'math': 80};
var {'math': math, 'science': science} = marks;
print('$math $science');
}Unhandled exception:
Bad state: Pattern matching errorThis 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
void main() {
Map<String, int> marks = {'math': 80};
// if (marks case {}) {} // ✗
}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 nameshorthand, instead ofisplus 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); usewhenfor 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:
{'customer': 'Ali', 'total': 1200}
{'customer': 'Sara', 'total': 600, 'notes': 'Ring the bell'}
{'total': 900}Your program should print exactly:
Free delivery for Ali
Delivery fee for Sara
RejectedTry it yourself before opening the solution.
Show solution
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');
}
}
}Free delivery for Ali
Delivery fee for Sara
RejectedSara'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
- Write a class
Petwith afinal String name. StorePet('Milo')in anObjectvariable and usePet(name: var n)to printPet: Milo. - Rewrite question 1 with the
:varshorthand. - Given
Map<String, Object?> user = {'name': 'Zayan', 'age': 6};, use a map pattern to printZayan is 6.
Application
-
Write a class
Rectanglewithwidth,heightand a getterperimeter. ForRectangle(3, 4), read only the getter with an object pattern and printPerimeter: 14.0. -
Write a function
describe(Object value)that printsText of length Nfor aStringandNumber Nfor anint, andSomething elseotherwise. UseString(:var length). Call it with'Sara',42andtrue. -
Predict, then check, which of these maps match
{'id': int id}:{'id': 7},{'id': 7, 'name': 'Ali'},{'name': 'Ali'},{'id': '7'}. -
Create
Order(Customer('Ali'), 800)(classes as in section 7). Use one nested pattern to printAli spent 800.0. -
This line crashes when it runs. Rewrite it with
if-caseso it printsNo science:var {'math': math, 'science': science} = {'math': 80};
Challenge
- Given a list of maps
{'name': 'Ali', 'marks': 82},{'name': 'Sara', 'marks': 95},{'name': 'Hina'}, print the names with marks of90or more, and count the invalid maps. PrintTop: SaraandInvalid: 1. - Write classes
Circle(radius)andSquare(side), each with anareagetter. For the list[Circle(1), Square(2), Square(3)], use object patterns to add up the areas of the squares only and printSquare area: 13.0.
Practice Answers
Question 1 — Solution
class Pet {
final String name;
Pet(this.name);
}
void main() {
Object pet = Pet('Milo');
if (pet case Pet(name: var n)) {
print('Pet: $n');
}
}Pet: MiloQuestion 2 — Solution
class Pet {
final String name;
Pet(this.name);
}
void main() {
Object pet = Pet('Milo');
if (pet case Pet(:var name)) {
print('Pet: $name');
}
}Pet: MiloQuestion 3 — Solution
void main() {
Map<String, Object?> user = {'name': 'Zayan', 'age': 6};
if (user case {'name': String name, 'age': int age}) {
print('$name is $age');
}
}Zayan is 6Question 4 — Solution
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');
}
}Perimeter: 14.0Question 5 — Solution
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);
}Text of length 4
Number 42
Something elseQuestion 6 — Solution
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');
}
}
}{id: 7} matches, id 7
{id: 7, name: Ali} matches, id 7
{name: Ali} does not match
{id: 7} does not matchQuestion 7 — Solution
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');
}
}Ali spent 800.0Question 8 — Solution
void main() {
var marks = {'math': 80};
if (marks case {'math': var math, 'science': var science}) {
print('$math $science');
} else {
print('No science');
}
}No scienceQuestion 9 — Solution
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');
}Top: Sara
Invalid: 1Question 10 — Solution
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');
}Square area: 13.0The Circle fails the Square(...) pattern, so only 4.0 + 9.0 is added.
Quick Revision
- What two things does
Circle(radius: var r)do at once? - When can you write
Circle(:radius)withoutvar? - Can an object pattern read a computed getter?
- Does
{'name': String name}match a map with an extra'city'key? - Why does
{'age': int age}fail when'age'isnull? - Why is a map pattern in a plain
vardeclaration risky? - What does a
whenguard add to anif-case?
Key Takeaways
- An object pattern
Circle(radius: var r)checks the type and reads getters in one step. :var radiusis shorthand forradius: var radius; plain:radiusworks only invar/finaldeclarations.- Object patterns read any getter: fields, computed getters, and built-in ones like
lengthandisEven. - 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; addwhenfor 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.