Pulling values out of complex data or branching logic cleanly has become more practical with the language’s evolution. Dart 3 introduces two features that make everyday code more expressive and less repetitive. Records let you bundle multiple values, and patterns let you unpack and match them.
Record Destructuring
Records in Dart 3 change how you handle grouped data. Instead of creating a small class just to return two or three related values, a record lets you package them in a single lightweight value. This makes it easier to pass structured information between functions without adding layers of code. Records keep strong typing, work with both positional and named fields, and can be compared for equality based on their contents. They also behave predictably in collections like sets or maps, which makes them handy for compact data transfer or quick lookups.
What Records Are
A record is a fixed container that can hold multiple values, each with its own type. Those values are ordered when declared positionally or labeled when declared with names. The compiler enforces the structure so that the record’s shape and type match what you expect anywhere in the code.
The record above carries two positional fields, a city name and a population count. You can access them directly as $1 and $2, but records become much more useful when destructured later. A record with named fields looks slightly different and reads more naturally.
This second form gives context to the data without needing to remember the order of fields. When printed or compared, records include their names and values, making debugging more transparent. Dart treats two records with the same shape and identical values as equal, even if they came from different parts of a project.
How to Destructure Records
Destructuring means breaking a record apart into individual variables in one step. Instead of manually pulling values out, you can unpack them directly into locals that match the record’s shape.
The two variables are created instantly and typed according to the record definition. You don’t have to declare their types, though you can if you prefer more explicit code.
Named records work with the same idea but use field labels when unpacking.
Destructuring also works in variable declarations that need to extract only some values. For example, you can ignore unused fields with an underscore.
That pattern avoids declaring unnecessary variables while still keeping the record structure intact. Destructuring fits naturally anywhere you would unpack tuples or composite results in other languages but with Dart’s type safety intact.
When Destructuring Records Helps
Returning multiple values from a function is a common need, and records make that direct and tidy. Imagine a method that reads configuration data from a JSON file and needs to return both the file path and a timestamp. With records, you can keep that operation lightweight.
Without records, you would either return a list, which loses typing, or define a small class that adds extra code for no real gain. Records keep the function compact while staying type-safe.
Destructuring also helps when chaining data through several operations. Suppose you have a process that fetches data, transforms it, and finally writes it to disk. Each step can return a record carrying multiple results that the next step immediately unpacks.
The pattern makes code easier to follow because you can read the unpacking as part of the function’s logic rather than as extra lines below it.
Limitations to Keep in Mind
Records in Dart are flexible but have a few constraints that are worth remembering. They’re static structures, which means you can’t dynamically change their shape once declared. You also can’t destructure them directly inside a method’s parameter list yet, though that feature could be added in future versions.
Instead, take the record as a single parameter and unpack it inside the body.
Null safety applies to records too. If a record itself or any of its fields can be null, Dart prevents direct destructuring without checking first. You can use a null check or pattern matching syntax to safely unpack optional records.
Something else worth knowing is that records don’t support methods or inheritance. They’re pure value containers meant for grouping data, not for encapsulating behavior. For anything that requires behavior or complex relationships, a class is still the right choice.
While these limitations exist, records fill an important gap between basic tuples and full-blown classes, giving Dart a clear way to handle grouped data and make destructuring a first-class pattern in daily coding.
Pattern Matching and Switch Expression
Pattern matching in Dart 3 changes how data is unpacked and how control flow works. Instead of writing long chains of conditionals or manually checking object types, patterns let you describe the structure of data directly. The compiler then checks if a value fits that structure and binds the parts you need. This brings a sense of structure to how you handle complex objects, collections, and records. Switch expressions make the idea even more expressive by letting you match and return results in one expression.
What Patterns Bring
Patterns match shape and bind values in one move. They work across records, lists, maps, and even classes. You can match types, destructure objects, and run different branches depending on what fits the pattern. The language does all of the structural checking for you, so you spend less time writing manual type casts or nested conditionals.
This code treats each branch as a structural match. The case int count syntax both checks the type and binds it to a variable. That combination of matching and destructuring in one step is what gives patterns their expressiveness.
Patterns also help in situations where values need to be unpacked differently based on structure. For instance, when working with configuration data that can vary depending on environment, you can use a pattern to pick out specific structures rather than check manually for map keys or types.
The compiler checks the structure of each case, so it matches only when the right keys and types are present.
Using Patterns for Destructuring Simple Values
Patterns make destructuring lists, maps, and records much smoother. You can unpack nested structures directly into variables that match the same layout. This reduces boilerplate and keeps variable binding tied to structure rather than position alone.
The list above unpacks into separate variables. You can also skip certain elements or collect the remaining ones with the spread syntax.
Destructuring maps is equally expressive. Patterns match keys and types at the same time, so there’s no need to check manually before extracting values.
This unpacking works well for configurations, API responses, or any context where you deal with consistent key-value structures. It makes the code more direct, so what you intend to read from the structure is immediately visible in the declaration.
Patterns with Switch Expressions
Switch expressions let you return results directly from pattern matches, turning what used to be several lines of code into a concise value-producing expression. You can match on both type and structure, combine conditions, and use guards to refine matches further.
The function above checks for type and extracts fields in one motion. Each branch matches the pattern for a subclass and binds its internal value. This form is more compact and safer than using manual type checks or downcasting.
Switch expressions can also work with primitive values and conditions combined with when.
You can even mix structural and value matches. That lets your logic stay descriptive while staying concise, something that becomes useful in systems with many data variants.
Patterns for Record Destructuring in Switch
Records and switch expressions fit naturally together. Since a record has a defined structure, a pattern can destructure it directly within the case. That reduces both boilerplate further as well as the need for intermediate variable assignments.
Each branch here matches the record’s layout. The compiler makes sure that only records with the correct structure can match. Adding a guard with when allows for fine-tuned control, matching specific ranges or attributes.
Records with named fields work the same way but with field patterns instead of positional ones.
This method is both readable and safe, as each variable you bind is automatically typed from the record definition. It’s common in Dart 3 for functions that return records to be unpacked this way inside switch expressions when the goal is to return a message or perform selective logic.
Tips for Clear Code with Patterns
Patterns make code more expressive, but their clarity depends on how they’re written. Keeping pattern logic simple helps others read your code without confusion. Bind only what’s needed and name variables meaningfully to avoid clutter.
When matching classes, bind fields explicitly rather than using var for everything. This makes your intent visible and types self-explanatory.
Using explicit field names inside patterns keeps the reader grounded in what’s happening. Avoid deeply nested patterns unless necessary, as they can hide logic behind structure rather than making it more readable.
You can also combine patterns with control flow like if-case to make matches feel more natural outside of switches.
This pattern keeps the same idea as switch expressions but gives you the freedom to use it inline. As with records, the compiler verifies that each branch matches the expected structure, so you can trust the bound variables immediately after the match.
Patterns work best when they match real data shapes directly without excessive layers of logic. Keeping them focused on binding what you truly need makes them one of the most expressive additions in Dart 3.
Conclusion
Destructuring and pattern matching in Dart 3 make working with data far more natural. Records help you group values without extra code, and patterns let you unpack and match that data in a way that keeps logic easy to follow. They fit neatly into how developers already think about structure and control flow, turning what used to take several lines into something concise and expressive. These features make everyday Dart code more direct, easier to read, and better aligned with how data actually moves through a project.






















