Dart Pattern Matching Guide

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.

  1. 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
  2. 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:

  1. data is a non-empty Map
  2. The Map contains a “user” key
  3. The value mapped to “user” is a List with exactly two elements
  4. First list element is a String, second is an integer
  5. 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

Leave a Reply

Your email address will not be published. Required fields are marked *