Explain Dart records
3 min read
Dart 3
| Aspect | Record | Class |
|---|---|---|
| Has a name | ❌ Anonymous | ✅ |
| Mutable | ❌ Always immutable | Configurable |
| Equality | ✅ Structural (auto) | ❌ Manual override |
| Can have methods | ❌ Just data | ✅ |
| Inheritance | ❌ | ✅ |
| Best for | Multi-return, light tuples, map keys | Domain objects, behaviour |
Two records with the same shape and same values are ==. That's the killer feature.
Code in action
// Positional
final pt = (3, 4);
pt.$1; // 3
pt.$2; // 4
// Named
final user = (name: 'Alice', age: 30);
user.name; // 'Alice'
user.age; // 30
// Mixed
final mixed = (1, 2, label: 'pair');
// Types
(int, int) point() => (3, 4);
({double lat, double lng}) coord() => (lat: 12.97, lng: 77.59);
// Destructuring
final (x, y) = point();
final (:lat, :lng) = coord();
final (first, _, third) = (1, 2, 3); // skip middle with _
// Free structural equality
(1, 'a') == (1, 'a'); // true 🎉
final s = <(int, String)>{(1, 'a'), (1, 'a')};
s.length; // 1 — deduped automatically
Killer use case: multi-value returns
// Before — boilerplate class or out-params
class _MinMax { final int min, max; _MinMax(this.min, this.max); }
// Dart 3 — clean and self-documenting
(int min, int max) findMinMax(List<int> xs) =>
(xs.reduce(min), xs.reduce(max));
final (min, max) = findMinMax([3, 1, 4, 1, 5, 9]); // (1, 9)
Combined with pattern matching:
String describe((int, int) p) => switch (p) {
(0, 0) => 'Origin',
(0, _) => 'On Y-axis',
(_, 0) => 'On X-axis',
(var x, var y) when x == y => 'On diagonal',
(var x, var y) => 'Point ($x, $y)',
};
When to reach for records vs classes
| You need… | Use |
|---|---|
| Return more than one value from a function | Record |
A composite key for a Map or Set | Record (structural equality) |
| Throw-away local grouping ("zip" of two lists) | Record |
Reusable domain concept with behaviour (User.greeting()) | Class |
| Mutability or inheritance | Class |
| Stable JSON schema your team will keep evolving | Class (you'll add methods/validation) |
Rule of thumb: records for transient shapes, classes for first-class concepts.
Common mistakes to avoid
// ❌ Treating records like full-blown data classes
final user = (name: 'Alice', age: 30);
user.greet(); // ❌ records have no methods
// ❌ Mutating fields
user.name = 'Bob'; // ❌ records are immutable
// ❌ Overusing records and leaking implementation details
({String token, int expiresIn}) login() => ...;
// any rename breaks every caller. For long-lived API surfaces, use a class.
// ❌ Forgetting positional vs named field access
final r = (1, 2);
r.first; // ❌ no such getter — use r.$1
// ❌ Assuming type compatibility by shape only with named records
({int a, int b}) one = (a: 1, b: 2);
({int b, int a}) two = one; // ❌ types differ even if shape matches
// Names matter; order of names does not, but the *set* of names is part of the type.
// ❌ Using a record where a sealed class makes more sense
// "result" with success/failure variants → sealed class, not a record
Interview follow-ups
-
How is a record different from a tuple in other languages? It's structurally typed with both positional and named fields, and equality / hashCode are built in. It's not a wrapper around a List —
$1,$2access is just sugar over fields, not list indexing. -
What does "structural equality" mean for records? Two records are equal if they have the same shape (same positional arity and same named fields) and all corresponding values are
==. No need to writeoperator ==orhashCode. -
Can a record be used as a
Mapkey? Yes, and it's one of the best uses —(x, y) → cellfor grids,(userId, date) → scorefor compound keys. Because hashCode is structural, you get the behaviour you want for free. -
When would you choose a class over a record? When you need methods, named identity (
Uservs(String, int)), mutability, inheritance, or a stable API surface. Records shine inside a function or as a return value; classes shine as domain concepts.
How helpful was this content?
Please sign in to rate this article.