Dart Patterns are only available starting from Dart version 3.0 or higher
1. Core Purpose of Patterns
Patterns form an independent syntax category in Dart, sitting alongside statements and expressions. They describe the structural shape of values and serve two primary, combinable functions: Matching and Destructuring.
- Matching: Verify whether a value fits an expected structure
- Matches a specific constant value
- Matches a specified data type
- Matches a predefined structure (List/Map/Object/Record)
- Satisfies custom conditional logic
- Destructuring: Once a match succeeds, quickly extract nested fields/elements from the value and bind them to local variables
1. Constant Pattern Example
void main() {
int number = 1;
switch (number) {
case 1:
print('one'); // Constant pattern matching: number == 1
default:
print('other');
}
}Code language: Dart (dart)
Output
one
2. Nested Subpattern Matching
Patterns support nested subpatterns to validate layered internal structures step by step
void main() {
const a = 'a';
const b = 'b';
var obj = ['a', 'b'];
switch (obj) {
case [a, b]:
print('$a, $b');
default:
print('No match');
}
}Code language: Dart (dart)
Output
a, b
3. Basic Destructuring Demo
var numList = [1, 2, 3];
// Destructure list elements via List pattern and bind new variables
var [x, y, z] = numList;
print(x + y + z); // 6, x = 1, y = 2, z = 3Code language: PHP (php)
4. Combined Nested Pattern Matching + Destructuring
void main() {
var list = ['b', 99];
switch (list) {
// Logical OR pattern: first element is "a" or "b", bind second value to variable c
case ['a' || 'b', var c]:
print(c); // Outputs 99
}
}Code language: Dart (dart)
II. Common Pattern Use Cases
1. Local Variable Declaration
Used for destructuring assignment to declare multiple variables in one line
// Destructure nested Record + List pattern
var (str, [n1, n2]) = ('text', [10, 20]);
print(str); // text
print(n1 + n2); // 30Code language: Dart (dart)
2. Variable Reassignment (Swap values without temporary variables)
var (a, b) = ("left", "right");
(b, a) = (a, b); // Swap two variables directly
print('$a $b'); // right leftCode language: Dart (dart)
3. switch / if-case Branches (Most widely used, with when guards)
All case clauses use refutable patterns: code enters the branch on match failure falls through to the next case
Basic Multi-Type Case Matching
void testSwitch(dynamic obj) {
switch (obj) {
case 1:
print('Number one');
case >= 10 && <= 20:
print('Number between 10 and 20 inclusive');
case (var a, var b):
print('Record value: $a,$b');
default:
print('No matching case found');
}
}Code language: Dart (dart)
Logical OR Pattern: Reuse logic across multiple cases
enum Color { red, yellow, blue, green }
void main() {
Color color = Color.red;
bool isPrimary = switch (color) {
Color.red || Color.yellow || Color.blue => true,
_ => false,
};
print(isPrimary); // true
}
Code language: PHP (php)
when Guard Clauses (Add extra filter conditions after matching succeeds)
sealed class Shape {}
class Square { final double size; Square(this.size); }
class Circle { final double size; Circle(this.size); }
void checkShape(Shape shape) {
switch (shape) {
// Shared match logic for Square and Circle, add filter size > 0
case Square(size: var s) || Circle(size: var s) when s > 0:
print('Valid symmetric shape');
default:
print('Invalid shape');
}
}Code language: Dart (dart)
Key Difference: when Guard vs Regular if Inside Case
void compareWhen((int, int) pair) {
switch (pair) {
case (int a, int b):
if (a > b) print('First value larger');
// If if condition fails: entire switch terminates, skips remaining cases
case (int a, int b) when a > b:
print('[when guard] First value larger');
// If when condition fails: only skip current case, continue matching subsequent cases
case (int a, int b):
print('First value smaller or equal');
}
}
void main() {
print("===== Test pair (5, 2) a > b =====");
compareWhen((5, 2));
print("\n===== Test pair (2, 5) a < b =====");
compareWhen((2, 5));
}Code language: Dart (dart)
4. for / for-in Loops (Extremely useful for destructuring MapEntry)
void main() {
Map<String, int> hist = {'a': 23, 'b': 100};
// Full verbose syntax
for (var MapEntry(key: key, value: count) in hist.entries) {
print('$key:$count');
}
// Shorthand syntax: simplify key:key to :key
for (var MapEntry(:key, value: count) in hist.entries) {
print('$key:$count');
}
}Code language: Dart (dart)
Output
D:\dartdemo\firstdart>dart run
Building package executable...
Built firstdart:firstdart.
a:23
b:100
a:23
b:100
Code language: CSS (css)
III. Real-World Business Scenarios for Patterns
1: Multi-Return Functions (Record Destructuring)
Traditional verbose implementation
(String, int) userInfo() => ("Lily", 13);
void main() {
var info = userInfo();
var name = info.$1;
var age = info.$2;
}
Code language: Dart (dart)
Simplified pattern destructuring syntax
var (name, age) = userInfo();
// Destructure named field records
final (:name, :age) = getData(); // Returns record (name: "Doug", age:25)Code language: Dart (dart)
Full Working Example
(String, int) userInfo() => ("Lily", 18);
({String name, int age}) getData() => (name: "Doug", age: 25);
void main() {
{
var (name, age) = userInfo();
print("Name: $name, Age: $age");
}
// Inner name/age variables do not conflict with outer scope
{
final (:name, :age) = getData();
print("Name: $name, Age: $age");
}
}Code language: Dart (dart)
2: Destructure Class Instances (Object Patterns)
class Foo {
final String one;
final int two;
Foo({required this.one, required this.two});
}
void main() {
final Foo myFoo = Foo(one: "test", two: 66);
var Foo(:one, :two) = myFoo;
print('$one $two'); // test 66
}Code language: Dart (dart)
3: Algebraic Data Types (Sealed Classes + switch Expression Calculations)
Handle a group of related subclasses uniformly without scattered instance methods
import 'dart:math';
sealed class Shape {}
class Square implements Shape { final double length; Square(this.length); }
class Circle implements Shape { final double radius; Circle(this.radius); }
double calculateArea(Shape shape) => switch (shape) {
Square(length: var l) => l * l,
Circle(radius: var r) => pi * r * r,
};
Code language: Dart (dart)
Complete Practical Demo
import 'dart:math';
// Sealed parent class restricts subclasses to this file only
sealed class Shape {}
// Square subclass
class Square implements Shape {
final double length;
Square(this.length);
}
// Circle subclass
class Circle implements Shape {
final double radius;
Circle(this.radius);
}
// Unified area calculation with switch expressions + object pattern matching
double calculateArea(Shape shape) => switch (shape) {
// Match Square type, destructure length into variable l
Square(length: var l) => l * l,
// Match Circle type, destructure radius into variable r
Circle(radius: var r) => pi * r * r,
};
void main() {
// Create shape instances
Shape square = Square(5);
Shape circle = Circle(3);
// Calculate areas
double squareArea = calculateArea(square);
double circleArea = calculateArea(circle);
print("Square area: $squareArea"); // 25.0
print("Circle area: ${circleArea.toStringAsFixed(2)}"); // 28.27
}Code language: Dart (dart)
Runtime Output
D:\dartdemo\firstdart>dart run
Building package executable...
Built firstdart:firstdart.
Square area: 25.0
Circle area: 28.27Code language: CSS (css)
4: JSON Data Validation & Destructuring (Eliminate verbose nested conditional checks)
Implementation without patterns: bloated nested type checks
void oldCheck(Map data) {
if (data is Map<String, Object?> && data.containsKey('user')) {
var user = data['user'];
if (user is List && user.length == 2) {
String name = user[0] as String;
int age = user[1] as int;
print('$name $age');
}
}
}Code language: Dart (dart)
if-case pattern approach: Validate structure, check types, bind variables all in a single line
void newCheck(Map data) {
if (data case {'user': [String name, int age]}) {
print('User $name, age $age');
}
}Code language: Dart (dart)
This single pattern verifies all of the following simultaneously:
- data is a non-empty Map
- The Map contains a “user” key
- The value mapped to “user” is a List with exactly two elements
- First list element is a String, second is an integer
- Automatically bind name and age as local variables
Full usage demo
void main() {
// 1. Valid JSON structure: match succeeds
final validJson = {
'user': ['Lily', 13]
};
newCheck(validJson); // Output: User Lily, age 13
// 2. Mismatched inner type: age stored as string → match fails
final wrongTypeJson = {
'user': ['Tom', '16']
};
newCheck(wrongTypeJson); // No console output
// 3. User array missing second element → match fails
final shortJson = {
'user': ['Jack']
};
newCheck(shortJson); // No console output
// 4. Entire "user" key missing → match fails
final noUserJson = {
'info': ['Bob', 20]
};
newCheck(noUserJson); // No console output
}
// Use if-case Map + List pattern to validate structure, types and destructure in one step
void newCheck(Map data) {
if (data case {'user': [String name, int age]}) {
print('User $name, age $age');
} else {
print('Invalid data format');
}
}Code language: PHP (php)
Dart Pattern Matching Guide