Watch the video (12 min)
On this page25 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What Is a Record?
  5. 2. Reading Positional Fields: $1, $2
  6. 3. Record Types
  7. 4. Named Fields
  8. 5. Mixing Positional and Named Fields
  9. 6. Returning Several Values From a Function
  10. 7. Records Are Immutable
  11. 8. Records Compare by Their Fields
  12. 9. Records in Lists and Maps
  13. 10. Naming a Record Type With typedef
  14. 11. Records vs Classes
  15. 12. A First Look at Destructuring
  16. 13. A Real-World Example
  17. Code Examples
  18. Common Beginner Mistakes
  19. Best Practices
  20. Mini Challenge
  21. Practice Questions
  22. Practice Answers
  23. Quick Revision
  24. Key Takeaways
  25. What's Next?

Lecture 76 — Records

Category12 — Generics & Advanced Types
Lecture076 of 140
Estimated video length12 min
TargetDart 3.12.2
LevelIntermediate

A function returns only one value, and pairing a name with a mark meant a List<Object>, a map, or a whole class. A record bundles a few values of different types into one value, with no class to write.

Contents

Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways

Every error message in this lecture is real. Each one was produced by running the code shown on Dart 3.12.2, so you will recognize it when you meet it yourself.


Prerequisites

Students should already understand, from Lectures 031, 033, 039, 049, 053, 060 and 071:

  • returning a value from a function, and named parameters with { }
  • lists and maps, including List<int> and Map<String, int>
  • classes with fields and constructors
  • overriding operator == and hashCode to compare objects by value
  • typedef for giving a type a short name

Connection With Previous Lectures

Ali wants a function that gives back the lowest and the highest of his marks. A function returns one value, so in Lecture 031 we could only do this:

dart
List<int> lowHigh(List<int> marks) {
  var sorted = [...marks]..sort();
  return [sorted.first, sorted.last];
}

void main() {
  var result = lowHigh([80, 65, 92]);
  print('Low ${result[0]}, high ${result[1]}');
}
output
Low 65, high 92

It works, but nothing says that index 0 is the low mark, and the list could hold three values or none. Pair a name with a mark and it gets worse: ['Ali', 80] is a List<Object> (Lecture 039). A class would fix both problems, but writing one just to carry two values back from a function is a lot of ceremony. Dart has a lighter tool.


Learning Objectives

By the end of this lecture you will be able to:

  • Create records with positional fields and with named fields
  • Read fields with $1, $2 and with field names
  • Write record types such as (String, int) and ({String name, int marks})
  • Return several values from one function
  • Explain why records are immutable
  • Compare records with == and use them in sets, lists and maps
  • Name a record type with typedef
  • Decide when a record is enough and when you need a class

1. What Is a Record?

A record is a single value that holds a fixed number of other values, called its fields. You write one by putting values in round brackets, separated by commas.

Analogy — a record is like a parcel. It may contain a book and a pen, but you hand it over, store it or return it as one thing.

dart
void main() {
  var student = ('Ali', 80);
  print(student);
}
output
(Ali, 80)

student is one value with two fields: a String and an int.


2. Reading Positional Fields: $1, $2

Fields without a name are positional. Read them with $1 for the first, $2 for the second, and so on:

dart
void main() {
  var student = ('Ali', 80);
  print(student.$1);
  print(student.$2 + 5);
}
output
Ali
85

Counting starts at 1, not 0 as with list indexes. Each field keeps its own type, so student.$2 is an int and maths works on it directly.


3. Record Types

The type of a record lists its field types in round brackets. ('Ali', 80) has the type (String, int):

dart
void main() {
  (String, int) student = ('Ali', 80);
  print(student.runtimeType);
  student = ('Sara', 92);
  print(student);
}
output
(String, int)
(Sara, 92)

A new record can replace the old one, as long as it has the same shape: the same number of fields with the same types, in the same order. Swapping the order is a different type:

dart
void main() {
  // (String, int) student = (80, 'Ali'); // ✗
}
output
Error: A value of type '(int, String)' can't be assigned to a variable of type '(String, int)'.

4. Named Fields

$1 and $2 don't say what a value means. Like named parameters in Lecture 033, a field can have a name. Write name: value inside the brackets, and read the field with .name:

dart
void main() {
  ({String name, int marks}) student = (name: 'Sara', marks: 92);
  print(student.name);
  print(student.marks);
  print(student);
}
output
Sara
92
(marks: 92, name: Sara)

In the type, named fields go inside curly braces: ({String name, int marks}). Their order doesn't matter, which is why Dart printed marks first. Their names do matter: they are part of the type.

dart
void main() {
  // ({String name, int marks}) s = (name: 'Ali', mark: 80); // ✗
}
output
Error: A value of type '({int mark, String name})' can't be assigned to a variable of type '({int marks, String name})'.

5. Mixing Positional and Named Fields

One record can have both kinds. Positional fields are still counted $1, $2, …, skipping the named ones:

dart
void main() {
  var child = ('Zayan', 'Lahore', age: 6);
  print(child.$1);
  print(child.$2);
  print(child.age);
  print(child.runtimeType);
}
output
Zayan
Lahore
6
(String, String, {int age})

In practice, pick one style per record. Named fields read best once there are more than two.


6. Returning Several Values From a Function

Here is the job from the start of the lecture. The return type is a record type, and the return builds a record:

dart
({int low, int high}) lowHigh(List<int> marks) {
  var sorted = [...marks]..sort();
  return (low: sorted.first, high: sorted.last);
}

void main() {
  var result = lowHigh([80, 65, 92]);
  print('Low ${result.low}, high ${result.high}');
}
output
Low 65, high 92

The function's first line now tells the reader exactly what comes back: two ints called low and high. No index to remember, and no class to write.


7. Records Are Immutable

Once a record is made, its fields can't change. There is no setter for $2 or .marks:

dart
void main() {
  var student = ('Ali', 80);
  // student.$2 = 90; // ✗
  student = (student.$1, 90); // ✓ build a new record
  print(student);
}
output
(Ali, 90)

The commented line gives:

output
Error: The setter '$2' isn't defined for the type '(String, int)'.

The variable can point to a new record (unless it is final), but each record itself stays exactly as it was built. That makes records safe to pass around: no function can quietly change one behind your back.


8. Records Compare by Their Fields

In Lecture 060 two Student objects with the same data were not equal until we overrode operator == and hashCode. Records do that for you. Two records are equal when they have the same shape and equal fields:

dart
void main() {
  print(('Ali', 80) == ('Ali', 80));
  print((name: 'Ali', marks: 80) == (marks: 80, name: 'Ali'));
  print(('Ali', 80) == (80, 'Ali'));
  var seen = {('Ali', 80)};
  seen.add(('Ali', 80));
  print(seen.length);
}
output
true
true
false
1

Named fields match by name, so their written order doesn't matter. Positional fields match by position. Because hashCode comes free as well, the set keeps only one copy.


9. Records in Lists and Maps

A record type works anywhere a type does, including inside < >:

dart
void main() {
  List<(String, int)> results = [('Ali', 80), ('Sara', 92), ('Zayan', 65)];
  for (var r in results) {
    print('${r.$1}: ${r.$2}');
  }

  Map<(int, int), String> seats = {(1, 1): 'Ali', (1, 2): 'Sara'};
  print(seats[(1, 2)]);
}
output
Ali: 80
Sara: 92
Zayan: 65
Sara

The map uses a record as its key: row and seat together. Lookup works because (1, 2) == (1, 2), as section 8 showed.


10. Naming a Record Type With typedef

Writing ({String name, int marks}) over and over is long. Lecture 071's typedef names it once:

dart
typedef Student = ({String name, int marks});

Student topOf(List<Student> students) {
  var best = students.first;
  for (var s in students) {
    if (s.marks > best.marks) best = s;
  }
  return best;
}

void main() {
  List<Student> students = [
    (name: 'Ali', marks: 80),
    (name: 'Sara', marks: 92),
  ];
  print(topOf(students).name);
}
output
Sara

Student is only an alias, so any record with the fields name and marks fits. No constructor is needed: the literal (name: 'Ali', marks: 80) is already a Student.


11. Records vs Classes

Both group values. They suit different jobs:

RecordClass
Needs a declaration?No — just ( )Yes — class with a constructor
Fields can change?NeverIf you allow it
== compares values?AutomaticallyOnly if you override it
Methods, getters, private fields?NoYes
Best forA few values returned or passed togetherA thing with behaviour and rules

A record for Lecture 060's point needs no code at all:

dart
void main() {
  var a = (x: 1, y: 2);
  var b = (x: 1, y: 2);
  print(a == b);
  print(a.x + b.y);
}
output
true
3

If the point needed a distanceTo method or a rule that x can't be negative, it would become a class.


12. A First Look at Destructuring

Reading result.low and result.high one by one is fine. Dart can also unpack a record into separate variables in one line:

dart
(int, int) lowHigh(List<int> marks) {
  var sorted = [...marks]..sort();
  return (sorted.first, sorted.last);
}

void main() {
  var (low, high) = lowHigh([80, 65, 92]);
  print('$low $high');
}
output
65 92

This is destructuring, the subject of Lecture 078 — Destructuring. It is built on patterns, which begin in Lecture 077.


13. A Real-World Example

Sara's shop keeps cart lines as records and returns a summary as a record:

dart
typedef Line = ({String item, double price, int qty});

({double total, int count}) summarize(List<Line> cart) {
  var total = 0.0;
  var count = 0;
  for (var line in cart) {
    total += line.price * line.qty;
    count += line.qty;
  }
  return (total: total, count: count);
}

void main() {
  List<Line> cart = [
    (item: 'Notebook', price: 250.0, qty: 2),
    (item: 'Pen', price: 40.0, qty: 5),
  ];
  var summary = summarize(cart);
  print('${summary.count} items, total ${summary.total}');
  print(cart.first == (item: 'Notebook', price: 250.0, qty: 2));
}
output
7 items, total 700.0
true

A typedef names the line shape, the function returns two values at once, and == checks a line by its contents. No class was written.


Code Examples

Example 1 — A Positional Record

dart
void main() {
  var city = ('Lahore', 13);
  print(city);
  print(city.$1);
}
output
(Lahore, 13)
Lahore

Example 2 — A Named Record

dart
void main() {
  var reading = (city: 'Karachi', celsius: 31.5);
  print('${reading.city}: ${reading.celsius}');
}
output
Karachi: 31.5

Example 3 — An Explicit Record Type

dart
void main() {
  (String, bool) task = ('Homework', false);
  task = (task.$1, true);
  print(task);
}
output
(Homework, true)

Example 4 — Three Fields

dart
void main() {
  var date = (2026, 10, 17);
  print('${date.$3}/${date.$2}/${date.$1}');
}
output
17/10/2026

Example 5 — Equality

dart
void main() {
  var a = (name: 'Zayan', age: 6);
  var b = (age: 6, name: 'Zayan');
  print(a == b);
  print(a == (name: 'Zayan', age: 7));
}
output
true
false

Example 6 — Records as Map Keys

dart
void main() {
  var board = {(0, 0): 'X', (1, 1): 'O'};
  print(board[(1, 1)]);
  print(board[(2, 2)]);
}
output
O
null

Example 7 — Student Marks

dart
typedef Result = ({String name, int marks});

void main() {
  List<Result> results = [
    (name: 'Ali', marks: 80),
    (name: 'Sara', marks: 92),
    (name: 'Zayan', marks: 45),
  ];
  for (var r in results) {
    print('${r.name}: ${r.marks >= 50 ? 'Pass' : 'Fail'}');
  }
}
output
Ali: Pass
Sara: Pass
Zayan: Fail

Example 8 — Shopping Total With Tax

dart
({double subtotal, double tax}) bill(double price, int qty) {
  var subtotal = price * qty;
  return (subtotal: subtotal, tax: subtotal * 0.1);
}

void main() {
  var b = bill(500, 3);
  print('Subtotal ${b.subtotal}, tax ${b.tax}');
}
output
Subtotal 1500.0, tax 150.0

Example 9 — Bank Balance

dart
(bool, double) withdraw(double balance, double amount) {
  if (amount > balance) return (false, balance);
  return (true, balance - amount);
}

void main() {
  var first = withdraw(1000, 300);
  print(first);
  var second = withdraw(first.$2, 900);
  print(second);
}
output
(true, 700.0)
(false, 700.0)

The function reports two things at once: whether it worked, and the new balance.

Example 10 — Temperature Range

dart
({double min, double max}) range(List<double> temps) {
  var min = temps.first;
  var max = temps.first;
  for (var t in temps) {
    if (t < min) min = t;
    if (t > max) max = t;
  }
  return (min: min, max: max);
}

void main() {
  var week = range([21.5, 30.0, 18.0, 27.5]);
  print('Min ${week.min}, max ${week.max}');
}
output
Min 18.0, max 30.0

Common Beginner Mistakes

1. Counting fields from zero

dart
void main() {
  var s = ('Ali', 80);
  // print(s.$0); // ✗
}
output
Error: The getter '$0' isn't defined for the type '(String, int)'.

Record fields start at $1.

2. Trying to change a field

dart
void main() {
  var s = ('Ali', 80);
  // s.$2 = 90; // ✗
}
output
Error: The setter '$2' isn't defined for the type '(String, int)'.

Build a new record instead: s = (s.$1, 90);.

3. Using $1 on a named field

dart
void main() {
  var s = (name: 'Ali', marks: 80);
  // print(s.$1); // ✗
}
output
Error: The getter '$1' isn't defined for the type '({int marks, String name})'.

Named fields are read only by name: s.name.

4. Forgetting the comma in a one-field record

dart
void main() {
  var s = (42);
  // print(s.$1); // ✗
}
output
Error: The getter '$1' isn't defined for the type 'int'.

(42) is just 42 in brackets. A one-field record needs a trailing comma: (42,).

5. Misspelling a field name in the type

(name: 'Ali', mark: 80) is not a ({String name, int marks}). The names are part of the type, as section 4 showed.


Best Practices

  • Use a record to return two or three related values from a function.
  • Prefer named fields once a record has more than two values or the meaning isn't obvious.
  • Give a record type a typedef when it appears in more than one place.
  • Switch to a class when the data needs methods, validation, or fields that change.
  • Use records as map keys or set items when a pair of values is the identity, like a seat.
  • Keep records small; a record with eight fields is a class in disguise.

Mini Challenge

Write a function stats that takes a List<int> of marks and returns a named record with the total and the average (a double). Call it with Ali's marks [80, 75, 85].

Your program should print exactly:

output
Total 240, average 80.0

Try it yourself before opening the solution.

Show solution
dart
({int total, double average}) stats(List<int> marks) {
  var total = 0;
  for (var m in marks) {
    total += m;
  }
  return (total: total, average: total / marks.length);
}

void main() {
  var s = stats([80, 75, 85]);
  print('Total ${s.total}, average ${s.average}');
}
output
Total 240, average 80.0

/ always gives a double, so average fits its field type.


Practice Questions

Work through these in order — they run from beginner to application to challenge.

Beginner

  1. Create a record ('Sara', 18) and print its second field.
  2. Create a named record with item: 'Pen' and price: 40 and print Pen costs 40.
  3. Declare a variable of type (String, double) holding ('Lahore', 24.5) and print it.

Application

  1. Print whether (1, 2) == (1, 2) and whether (1, 2) == (2, 1).
  2. Write minMax(int a, int b) that returns (int, int) with the smaller value first. Print minMax(9, 4).
  3. Store List<(String, int)> with ('Ali', 80) and ('Sara', 92). Print each as Ali scored 80.
  4. Write typedef Point = ({int x, int y}); and a function Point moveRight(Point p) that returns a new point with x one larger. Print moveRight((x: 1, y: 5)).
  5. Put ('Ali', 80) into a set three times and print the set's length.

Challenge

  1. Write divide(int a, int b) that returns a named record ({int quotient, int remainder}). Print both fields for divide(17, 5) as 17 = 5 × 3 + 2.
  2. A bank stores accounts as Map<String, ({double balance, bool frozen})>. With Ali: (balance: 500.0, frozen: false) and Sara: (balance: 900.0, frozen: true), print only the names of accounts that are not frozen and have more than 100.0.

Practice Answers

Question 1 — Solution

dart
void main() {
  var person = ('Sara', 18);
  print(person.$2);
}
output
18

Question 2 — Solution

dart
void main() {
  var pen = (item: 'Pen', price: 40);
  print('${pen.item} costs ${pen.price}');
}
output
Pen costs 40

Question 3 — Solution

dart
void main() {
  (String, double) weather = ('Lahore', 24.5);
  print(weather);
}
output
(Lahore, 24.5)

Question 4 — Solution

dart
void main() {
  print((1, 2) == (1, 2));
  print((1, 2) == (2, 1));
}
output
true
false

Question 5 — Solution

dart
(int, int) minMax(int a, int b) => a < b ? (a, b) : (b, a);

void main() {
  print(minMax(9, 4));
}
output
(4, 9)

Question 6 — Solution

dart
void main() {
  List<(String, int)> scores = [('Ali', 80), ('Sara', 92)];
  for (var s in scores) {
    print('${s.$1} scored ${s.$2}');
  }
}
output
Ali scored 80
Sara scored 92

Question 7 — Solution

dart
typedef Point = ({int x, int y});

Point moveRight(Point p) => (x: p.x + 1, y: p.y);

void main() {
  print(moveRight((x: 1, y: 5)));
}
output
(x: 2, y: 5)

The old point is untouched; moveRight builds a new one.

Question 8 — Solution

dart
void main() {
  var seen = <(String, int)>{};
  seen.add(('Ali', 80));
  seen.add(('Ali', 80));
  seen.add(('Ali', 80));
  print(seen.length);
}
output
1

Question 9 — Solution

dart
({int quotient, int remainder}) divide(int a, int b) {
  return (quotient: a ~/ b, remainder: a % b);
}

void main() {
  var r = divide(17, 5);
  print('17 = 5 × ${r.quotient} + ${r.remainder}');
}
output
17 = 5 × 3 + 2

Question 10 — Solution

dart
void main() {
  Map<String, ({double balance, bool frozen})> accounts = {
    'Ali': (balance: 500.0, frozen: false),
    'Sara': (balance: 900.0, frozen: true),
  };
  accounts.forEach((name, account) {
    if (!account.frozen && account.balance > 100.0) {
      print(name);
    }
  });
}
output
Ali

Quick Revision

  1. How do you write a record with two positional fields?
  2. Does $1 read the first field or the second?
  3. What is the type of (name: 'Ali', marks: 80)?
  4. Can you change a field of a record after it is made?
  5. When are two records equal?
  6. How do you write a record with only one field?
  7. How does typedef help with record types?
  8. When should you use a class instead of a record?

Key Takeaways

  • A record groups a fixed number of values, of any types, into one value: ('Ali', 80).
  • Positional fields are read with $1, $2, …; named fields with .name.
  • Record types look like (String, int) or ({String name, int marks}); field names and order of positional fields are part of the type.
  • Records let a function return several values without writing a class.
  • Records are immutable: build a new record to change a value.
  • == and hashCode compare records by their fields, so they work in sets and as map keys.
  • typedef gives a record type a short name.
  • Use records for small groups of data; use classes when data needs behaviour or rules.

What's Next?

Lecture 77 — Introduction to Patterns. This closes Category 12. Section 12 gave a glimpse of var (low, high) = ..., which pulled a record apart in one line. That line is a pattern. Next we will learn what patterns are, where Dart lets you use them, and how they check and unpack values, starting Category 13 — Patterns & Modern Dart.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.