Skip to main content
A pattern matches a value based on its runtime type if the value is an instance of the specified type or a subtype. This type-checking behavior is fundamental to Variable Patterns and Wildcard Patterns (via explicit annotations) and is intrinsic to Object Patterns. Upon a successful match, the pattern casts the value to the matched type, allowing for type-safe binding or destructuring.

Syntax

Type matching occurs in three primary syntactic forms:
// 1. Variable Pattern with Type Annotation
// Checks type, casts, and binds to 'variableName'
<Type> <variableName>

// 2. Wildcard Pattern with Type Annotation
// Checks type only (no binding)
<Type> _

// 3. Object Pattern
// Checks that the value is 'Type' (or subtype) before matching subpatterns
<Type>(<subpatterns>)

Mechanics

1. Runtime Type Check

The pattern evaluates the matched value against the specified <Type>. This is functionally equivalent to the expression value is <Type>.
  • Annotated Patterns: int i checks if the value is an int.
  • Object Patterns: Square(area: var a) first checks if the value is a Square before attempting to access the area getter.

2. Auto-Casting

If the type check succeeds, the value is automatically cast to <Type>. This ensures that subsequent operations within the pattern’s scope (or subpatterns within an Object Pattern) treat the value as the specific type.

3. Binding vs. Discarding

  • Variable Pattern: The cast value is assigned to the new variable, scoped to the control flow structure.
  • Wildcard Pattern: The type is validated, but the result is discarded.
  • Object Pattern: The value is cast to allow extraction of fields defined on that type.

Examples

Variable and Wildcard Patterns

These patterns use explicit annotations to enforce type constraints.
dynamic obj = 10;

switch (obj) {
  // Variable pattern: checks type, binds to 'i'
  case int i: 
    print(i.isEven); 
  
  // Wildcard pattern: checks type, discards value
  case String _:
    print('Matches String');
}

Object Pattern

The Object Pattern implicitly performs a type check against the class name provided.
abstract class Shape {}
class Rect extends Shape { double width; Rect(this.width); }
class Circle extends Shape { double radius; Circle(this.radius); }

Shape shape = Rect(10);

switch (shape) {
  // Checks if shape is runtime type 'Rect'. 
  // If true, casts to Rect and accesses 'width'.
  case Rect(width: var w): 
    print('Rectangle width: $w');
    
  case Circle(radius: var r):
    print('Circle radius: $r');
}

Nullability Rules

Type matching strictly enforces nullability.
  • Non-nullable Type: A pattern <Type> (e.g., int) fails if the value is null.
  • Nullable Type: A pattern <Type>? (e.g., int?) matches if the value is of the specified type or null.
int? nullableValue = null;

switch (nullableValue) {
  case int i:
    print('Matches integer'); // Fails (value is null)
  case int? i:
    print('Matches integer or null'); // Succeeds
}

Generic Types and Reification

When matching against generic types (e.g., List<int>), the pattern validates the runtime type of the container. Because Dart generics are reified, type arguments are preserved and checked at runtime. The pattern List<int> list matches only if the object was instantiated as List<int> (or a subtype). It does not inspect the contents of a List<dynamic> to see if they happen to be integers.
// Instantiated explicitly as List<dynamic>
List<dynamic> dynamicList = <dynamic>[1, 2, 3]; 

// Instantiated explicitly as List<int>, but stored in a List<dynamic> variable
List<dynamic> intList = <int>[1, 2, 3]; 

switch (dynamicList) {
  case List<int> list:
    // NOT selected. Runtime type is List<dynamic>.
    print('List<int>'); 
  case List<dynamic> list:
    // Selected.
    print('List<dynamic>');
}

switch (intList) {
  case List<int> list:
    // Selected. Runtime type is List<int>.
    print('List<int>');
}
Master Dart with Deep Grasping Methodology!Learn More