Dart Record

1. What Is a Record?

A Record is an anonymous, immutable, composite aggregate type that bundles multiple pieces of data of different types into a single object. Here are its core differences compared to List / Set / Map:

  • Fixed length: The number of fields cannot be added or removed once instantiated; heterogeneous storage support:
  • A single Record can hold values of entirely distinct types such as int, String, and bool
  • Strict static typing: The compiler recognizes each field’s type, preserving full type safety
  • Read-only immutability: Field values cannot be reassigned after creation

Records are fully valid variables and support all standard operations: variable assignment, function parameters / multi-value returns, nesting, and storage inside List/Map/Set.

2. Basic Record Syntax

1. Record Literals

Records are wrapped in parentheses with comma-separated entries, split into two categories: positional fields and named fields. Positional fields come first, while named fields prefixed with key: follow afterward.

// Mixed positional and named fields
var info = ('Jim', age: 20, flag: true, 'Student');Code language: JavaScript (javascript)

2. Record Usage Patterns

1 Positional Fields

// Declaration: two positional fields, String then int
(String, int) user;

// Value assignment matching the structure
user = ('Li Si', 22);
Code language: JavaScript (javascript)

2 Named Fields

Wrap type declarations inside {}

// Declare two named fields a(int), b(bool)
({int a, bool b}) data;

data = (a: 100, b: false);Code language: JavaScript (javascript)

3 Mixed Usage

Combine positional and named field type annotations

// 1 positional String field, 2 named fields score(int) and pass(bool)
(String, {int score, bool pass}) exam;
exam = ('Final Exam', score: 90, pass: true);
Code language: JavaScript (javascript)

3. Common Pitfalls

1: Named field names form part of the type structure; different names create entirely separate types
// Type: ({int a, int b})
({int a, int b}) pointAB = (a: 1, b: 2);
// Type: ({int x, int y})
({int x, int y}) pointXY = (x: 3, y: 4);

// Compile error! Type mismatch, cross-assignment is invalid
// pointAB = pointXY;
Code language: JavaScript (javascript)

For named fields, even if both contain two int values, differing field names (ab vs xy) result in incompatible types and block assignment. This rule does not apply to positional fields, explained below.

2: Aliases on positional fields in type annotations act only as documentation and do not affect type matching

Labels assigned to positional fields within parentheses are human-readable documentation ignored by the compiler. Values can be freely assigned between Records with identical underlying structures:

(int a, int b) p1 = (10, 20);
(int x, int y) p2 = (30, 40);

// Valid assignment: both are two-int positional Records with identical underlying types
p1 = p2;Code language: JavaScript (javascript)

This mirrors function parameters: renaming parameters does not alter a function’s signature, following identical logic.

4. Accessing Fields Inside a Record

Records are immutable; they only expose getters with no setters, so field values cannot be modified.

  1. Named fields: Retrieve values directly with .fieldName
  2. Positional fields: Access via $numericIndex. The index counts only positional fields and skips all named fields entirely
var record = ('first', a: 2, b: true, 'last');

print(record.$1); // First positional field: first
print(record.a);  // Named field a: 2
print(record.b);  // Named field b: true
print(record.$2); // Second positional field: last
Code language: PHP (php)

5. Rules for Record Structural Type Matching

Records do not require standalone class definitions; type equality is determined entirely by their structural shape:

Shape = total field count + type of every field + names of all named fields

Two Records count as the same type if their shapes fully match, even if they originate from separate files or external libraries.

The compiler tracks static types for every field without losing any type information at runtime:

// 1st positional field: num type, 2nd positional field: Object type
(num, Object) pair = (42, "sample text");

var firstVal = pair.$1; // Static type num, runtime value int
var secondVal = pair.$2;// Static type Object, runtime value StringCode language: JavaScript (javascript)

6. Record Equality Check with ==

Two Records evaluate as equal only when both conditions below hold true:

  1. The two Records have completely identical shapes
  2. All corresponding positional and named fields hold equal values

The written order of named fields does not impact equality checks; however, mismatched named field names immediately create different types and guarantee a false equality result.

// Positional fields with matching shape and values → evaluates to true
(int x, int y, int z) point = (1, 2, 3);
(int r, int g, int b) color = (1, 2, 3);
print(point == color); // true

// Different named field names create incompatible types → evaluates to false
({int x, int y, int z}) p = (x:1, y:2, z:3);
({int r, int g, int b}) c = (r:1, g:2, b:3);
print(p == c); // falseCode language: PHP (php)

Records come with auto-generated overrides for == and hashCode, requiring no manual implementation from developers.

7. Multi-Return Function Values

Dart functions only support a single return value. Records bundle multiple values of distinct types, paired with pattern destructuring to unpack values into standalone variables cleanly—an ideal replacement for List/Map without sacrificing type safety.

1 Positional Field Destructuring

// Returns name(String), age(int)
(String name, int age) getUserInfo(Map<String, dynamic> json) {
  return (json['name'] as String, json['age'] as int);
}

void main() {
  final userJson = {'name': 'Dash', 'age': 10, 'color': 'blue'};
  // Direct one-line destructuring into two separate variables
  var (userName, userAge) = getUserInfo(userJson);
  print(userName); // Dash
  print(userAge);  // 10
}
Code language: JavaScript (javascript)

2 Named Field Destructuring

Syntax: :fieldName

({String name, int age}) getUserInfo(Map<String, dynamic> json) {
  return (name: json['name'] as String, age: json['age'] as int);
}

void main() {
  final userJson = {'name': 'Dash', 'age': 10};
  // Named destructuring syntax
  final (:name, :age) = getUserInfo(userJson);
  print(name);
}
Code language: JavaScript (javascript)

Drawbacks of Alternatives

Languages such as C++, C#, and Java lack Record types and require the following cumbersome workarounds:

  1. Define a custom class: Verbose boilerplate code for constructors and field declarations;
  2. Return a List: Eliminates static type safety, casting all values to dynamic;
  3. Return a Map: Prone to typos in string keys, with no compile-time type validation.

8. Lightweight Structured Data Containers

Use Records directly when you only need to store data with no associated methods, eliminating the boilerplate required to define full classes. This makes Records perfect for bulk list datasets.

Example

List of page button configurations (this Flutter example is for demonstration; the logic will become clearer once you learn Flutter):

import 'package:flutter/material.dart';

void main() {
  final buttonList = [
    (
      label: "Upload File",
      icon: const Icon(Icons.upload_file),
      onPressed: () => print("Upload button tapped"),
    ),
    (
      label: "View Details",
      icon: const Icon(Icons.info),
      onPressed: () => print("Details button tapped"),
    )
  ];
}
Code language: JavaScript (javascript)

9. Type Aliases for Records with typedef

Simplify lengthy type signatures

Record type definitions grow verbose quickly. The typedef keyword creates reusable type aliases that simplify future unified structural edits.

Basic Usage
// Create alias for button Record; onPressed accepts null values
typedef ButtonConfig = ({
  String label,
  Icon icon,
  void Function()? onPressed
});

// Declare lists directly using the alias
List<ButtonConfig> buttons = [
  (label: "Submit", icon: const Icon(Icons.check), onPressed: () {}),
  (label: "Cancel", icon: const Icon(Icons.close), onPressed: null),
];
Code language: PHP (php)
Future Migration Paths

If you later need to add custom methods and encapsulated logic to your data, your business iteration logic requires zero changes—only swap the underlying type:

  1. Solution 1: Migrate to a standard Class
class ButtonConfig {
  final String label;
  final Icon icon;
  final void Function()? onPressed;
  ButtonConfig({required this.label, required this.icon, this.onPressed});
  // New custom helper method
  bool get hasClickEvent => onPressed != null;
}
Code language: PHP (php)
  1. Solution 2: Wrap the original Record with an Extension Type
extension type ButtonConfig._(({String label, Icon icon, void Function()? onPressed}) _) {
  String get label => _.label;
  Icon get icon => _.icon;
  void Function()? get onPressed => _.onPressed;

  ButtonConfig({required String label, required Icon icon, void Function()? onPressed})
      : this._((label: label, icon: icon, onPressed: onPressed));

  bool get hasClickEvent => _.onPressed != null;
}
Code language: PHP (php)

Page rendering loops for buttons remain completely unmodified under both migration approaches.

10. Summary

  1. Version requirement: Dart 3.0 and above;
  2. Three core traits: anonymous, immutable, aggregate; fixed length, heterogeneous storage, statically typed;
  3. Two field varieties: positional fields accessed via $1/$2, named fields accessed via .fieldName;
  4. Type resolution: Structural shape defines type identity; named field names are part of shape checks, positional labels serve only as documentation;
  5. Equality rules: Returns true if shape matches and all field values are identical;
  6. Key advantages: Native multi-value function returns, lightweight data containers, full static type safety;
  7. Extensibility: typedef creates shorthand aliases, with seamless future migration to class or extension type implementations.

Leave a Reply

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