Exploring Dart 3.0's Powerful Pattern Matching and Destructuring

Exploring Dart 3.0's Powerful Pattern Matching and Destructuring

Dart is a versatile programming language and has introduced new features that enhance code expressiveness and development productivity. Among these features, pattern matching and destructuring stand out as powerful tools. In this article, we'll delve into the world of patterns in Dart, exploring their various use cases and showcasing real-world examples.

Patterns in Dart: Matching Values and Destructuring Objects

Patterns are a syntactic category in Dart, representing the shape of a set of values that can be matched against actual values. They serve two main purposes: pattern matching and pattern destructuring.

Pattern Matching

Pattern matching enables developers to test whether a value meets specific criteria, such as having a particular shape, being a constant, equaling something else, or having a specific type. Let's look at some real-world examples:

  1. Matching a Shape:

     import 'dart:math';
     class Rectangle { 
       final double width; 
       final double height; 
       Rectangle({required this.width, required this.height}); 
     class Hexagon {
       final double sideLength;
       Hexagon({required this.sideLength});
     void calculateArea(dynamic shape) { 
       switch(shape) {
         case Rectangle(width: double w, height: double h):
           final area = w * h;
           print('Area: $area');
         case Hexagon(sideLength: double l):
           final area = (3 * sqrt(3) * pow(l, 2)) / 2;
           print('Area: $area');
     void main() { 
       calculateArea(Rectangle(width: 5, height: 10)); // Area: 50 
       calculateArea(Hexagon(sideLength: 6.9)); // Area: 123.6944084225314

    In this example, we use pattern matching to check if the shape parameter is an instance of the Rectangle or Hexagon class, allowing us to calculate its area. If we'd tried this in Dart before v3.0, we'd get the error: "Case expressions must be constant", and "The argument type 'Type' can't be assigned to the parameter type 'double'."

  2. Matching a Constant:

     const String example = 'example.com';
     const String gmail = 'gmail.com';
     void validateEmail(String email) {
       final pattern = RegExp(r'^[\w-]+@([\w-]+\.)+[\w-]+$');
       final match = pattern.firstMatch(email);
       if (match != null) {
         final domain = match.group(1);
         switch (domain) {
           case example:
             print('Valid email from example.com');
           case gmail:
             print('Valid email from gmail.com');
             print('Valid email from another domain');
       } else {
         print('Invalid email format');
     void main() {
       validateEmail('john.doe@example.com'); // Output: Valid email from example.com
       validateEmail('jane.smith@gmail.com'); // Output: Valid email from gmail.com
       validateEmail('foo@bar.com'); // Output: Valid email from another domain
       validateEmail('invalid_email'); // Output: Invalid email format

    In this example, we're validating email addresses by matching the domain part of the email against different patterns. Depending on the domain, we print different messages indicating the validity of the email.

Pattern Destructuring

Pattern destructuring provides a convenient way to break down an object into its constituent parts, extracting values for further use. Let's explore some practical scenarios:

  1. Destructuring a List:

     void processCoordinates(List<double> coordinates) {
       var [x, y, z] = coordinates;
       print('x: $x, y: $y, z: $z');
     void main() {
       processCoordinates([10.5, 20.3, 5.1]); // Output: x: 10.5, y: 20.3, z: 5.1

    In this example, we destructure a list of coordinates into individual variables, enabling easy access and manipulation.

  2. Destructuring a Map Entry:

     void processUser(MapEntry<String, int> userEntry) {
       var {key: username, value: age} = userEntry;
       print('Username: $username, Age: $age');
     void main() {
       var user = MapEntry('JohnDoe', 25);
       processUser(user); // Output: Username: JohnDoe, Age: 25

    Here, we destructure a MapEntry object into separate variables representing the key and value. This allows us to access and utilize the user's username and age individually.

Common Use Cases for Patterns in Dart

  1. Simplifying Data Validation: Patterns prove valuable when validating complex data structures like JSON. They allow us to check if the structure conforms to our expectations and extract specific values effortlessly.

  2. Enhancing Switch Statements: Switch statements become more powerful with pattern matching, enabling multiple cases to share a body or matching against various patterns to execute specific code blocks based on input conditions.

  3. Efficient Variable Assignment: Patterns provide a concise and expressive way to declare and assign variables simultaneously, making code more readable and reducing redundancy.

What have we learned?

With pattern matching and destructuring, Dart empowers developers to write more expressive and concise code. Whether you're validating data, manipulating objects, or enhancing control flow, patterns offer a powerful and flexible toolset. By leveraging these features, you can improve your Dart programming skills and boost productivity in real-world scenarios.

Other information

I used another free credit at STOCKIMG.AI to generate a horizontal poster using disco diffusion for the article poster image. This time I used the image from my first post as the primer, with some words about pattern matching and destructuring. The image above is the generated result. This time I was expecting something similar and the result is interesting. Given the two article topics are similar, I thought I’d go ahead and use it.