Watch the video (12 min)
On this page26 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What Are Generics?
  5. 2. Reading the Angle Brackets
  6. 3. Type Parameters Are Placeholders
  7. 4. The Problem Without Generics: List<Object>
  8. 5. Casts Turn Compile Errors into Crashes
  9. 6. List<dynamic> Is Even Worse
  10. 7. Safety and Reuse — Both at Once
  11. 8. Type Inference for Generic Types
  12. 9. The Declared Type Fills the Slot Too
  13. 10. Different Type Arguments, Different Types
  14. 11. Generic Methods You Already Used
  15. 12. When to Write the Type Argument
  16. 13. Check<T> Revisited
  17. 14. A Real-World Example
  18. Code Examples
  19. Common Beginner Mistakes
  20. Best Practices
  21. Mini Challenge
  22. Practice Questions
  23. Practice Answers
  24. Quick Revision
  25. Key Takeaways
  26. What's Next?

Lecture 72 — Generics Fundamentals

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

A list that accepts anything can't promise anything, so every read needs a risky cast. Generics let one List serve every type while still promising exactly which type it holds: List<int>, List<String>, List<Student>.

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 010, 011, 039–049 and 071:

  • is and as, and the difference between Object and dynamic
  • lists, sets and maps, including List<Object> and the empty List<dynamic>
  • map, where, whereType<T>() and fold
  • simple classes, and the generic typedef Check<T>

Connection With Previous Lectures

Since Lecture 039 we have written angle brackets everywhere. Several times we hit a wall and postponed it to this lecture. Lecture 044 showed this puzzle:

dart
void main() {
  var marks = [80, 75];
  // print(marks.fold(0, (sum, mark) => sum + mark)); // ✗
}
output
Error: The operator '+' isn't defined for the type 'Object?'.

Why does fold work in a variable but not inside print? Why is [] a List<dynamic>? And why does List need brackets at all, when it could just hold anything? All three questions have one answer: generics.


Learning Objectives

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

  • Explain what generics are and the two problems they solve: safety and reuse
  • Read generic types such as List<int>, Set<String> and Map<String, int>
  • Distinguish a type parameter from a type argument
  • Explain why List<Object> and List<dynamic> lead to casts and run-time crashes
  • Predict the type Dart infers for a list, set or map literal
  • Recognize that List<int> and List<String> are different types
  • Pass explicit type arguments to generic methods: fold<T>, map<R>, whereType<T>
  • Decide when to write a type argument and when to let Dart infer it

1. What Are Generics?

A generic type is a type with an empty slot for another type. List is generic: on its own it means "a list of something". List<int> fills the slot: "a list of int".

Analogy — a factory makes identical storage jars, each with a blank label. One jar is labelled Sugar, another Salt. Same jar design, but the label tells everyone what may go inside — and what will come out.

dart
void main() {
  List<int> marks = [80, 75, 92];
  List<String> names = ['Ali', 'Sara', 'Zayan'];
  print(marks.runtimeType);
  print(names.runtimeType);
}
output
List<int>
List<String>

One List design, two labels. That is the whole idea.


2. Reading the Angle Brackets

Read the brackets aloud as "of":

TypeRead it as
List<int>a list of int
Set<String>a set of String
Map<String, int>a map from String keys to int values
List<List<int>>a list of lists of int

A map has two slots, separated by a comma — one for keys, one for values:

dart
void main() {
  Map<String, int> marks = {'Ali': 80, 'Sara': 92};
  marks['Zayan'] = 70;
  print(marks['Sara']);
  print(marks);
}
output
92
{Ali: 80, Sara: 92, Zayan: 70}

3. Type Parameters Are Placeholders

Inside Dart's own library, List is declared as List<E>, and its add method as add(E value). E is a type parameter: a placeholder name. When you write List<int>, int is the type argument, and every E becomes int. So marks.add now accepts only an int:

dart
void main() {
  List<int> marks = [80, 75];
  marks.add(92);
  // marks.add('ninety'); // ✗
  print(marks);
}
output
[80, 75, 92]

The commented line is rejected before the program runs:

output
Error: The argument type 'String' can't be assigned to the parameter type 'int'.
  marks.add('ninety');
            ^

Placeholders are single capital letters by convention: E for element, T for type, K and V for key and value (Map<K, V>), R for a result.


4. The Problem Without Generics: List<Object>

Imagine List had no slot and always held Object. Ali's marks would compile into the list, but nothing could be done with them:

dart
void main() {
  List<Object> marks = [80, 75, 92];
  var total = 0;
  for (final mark in marks) {
    // total = total + mark; // ✗
  }
  print(total);
}
output
Error: A value of type 'Object' can't be assigned to a variable of type 'num'.

Dart only knows each mark is an Object, and Object can't be added (Lecture 011).


5. Casts Turn Compile Errors into Crashes

The only way out is a cast with as (Lecture 010) — a promise you make to Dart. If a wrong value has slipped in, the promise breaks while the program is running:

dart
void main() {
  List<Object> marks = [80, 75, 'ninety'];
  var total = 0;
  for (final mark in marks) {
    total = total + (mark as int);
  }
  print(total);
}
output
Unhandled exception:
type 'String' is not a subtype of type 'int' in type cast

(Dart also prints a list of file locations, which we leave out.) Nothing warned us: the list accepted 'ninety', and the crash came later, far from the mistake.


6. List<dynamic> Is Even Worse

dynamic switches checking off completely (Lecture 011), so no cast is needed — and no mistake is caught:

dart
void main() {
  List<dynamic> names = ['Ali', 'Sara', 42];
  for (final name in names) {
    print(name.toUpperCase());
  }
}
output
ALI
SARA
Unhandled exception:
NoSuchMethodError: Class 'int' has no instance method 'toUpperCase'.
Receiver: 42
Tried calling: toUpperCase()

With List<String>, the 42 would have been rejected on the line that wrote it.


7. Safety and Reuse — Both at Once

Generics solve both problems. Safety: the list knows its element type, so wrong values are stopped at compile time and reads need no cast. Reuse: one List works for every type, including your own classes. Nobody had to write an IntList or a StudentList:

dart
class Student {
  final String name;
  final int marks;
  Student(this.name, this.marks);
}

void main() {
  List<Student> students = [Student('Ali', 80), Student('Sara', 92)];
  for (final s in students) {
    print('${s.name}: ${s.marks}'); // no cast needed
  }
}
output
Ali: 80
Sara: 92

Dart knows every element is a Student, so s.name just works.


8. Type Inference for Generic Types

With var, Dart reads the values and fills the slot for you (Lecture 005):

dart
void main() {
  var marks = [80, 75];
  var prices = [4, 12.5];
  var names = {'Ali', 'Sara'};
  var stock = {'Tea': 10, 'Milk': 5};
  var orders = [];
  print(marks.runtimeType);
  print(prices.runtimeType);
  print(names.runtimeType);
  print(stock.runtimeType);
  print(orders.runtimeType);
}
output
List<int>
List<num>
_Set<String>
_Map<String, int>
List<dynamic>

Dart picks the closest type the values share: int and double share num. The leading _ marks Dart's internal set and map classes (Lecture 047); read _Map<String, int> as Map<String, int>. An empty literal gives Dart nothing to read, so the slot becomes dynamic.


9. The Declared Type Fills the Slot Too

Inference also works the other way: from the declared type into the values. Here the 4 and 12 become double because the list must hold double:

dart
void main() {
  List<double> prices = [4, 12];
  List<String> orders = [];
  var basket = <String>[];
  print(prices);
  print(orders.runtimeType);
  print(basket.runtimeType);
}
output
[4.0, 12.0]
List<String>
List<String>

For an empty collection, give the type either on the variable or on the literal (<String>[]).


10. Different Type Arguments, Different Types

List<int> and List<String> are both lists, but they are not the same type:

dart
void main() {
  List<int> marks = [80, 75];
  print(marks is List<String>);
  // List<String> names = marks; // ✗
}
output
false
output
Error: A value of type 'List<int>' can't be assigned to a variable of type 'List<String>'.

How related types such as List<int> and List<num> behave is the subject of Lecture 075 — Variance & Type Relationships.


11. Generic Methods You Already Used

Methods can have type parameters too. You met three of them in Lectures 043 and 044: whereType<T>(), map<R>(...) and fold<T>(...). Usually Dart infers the type argument; you may also write it yourself:

dart
void main() {
  var marks = [80, 75, 92];
  List<Object> form = ['Sara', 17, 92.5];
  print(form.whereType<int>());
  print(marks.map<String>((m) => 'Mark $m').toList());
  print(marks.fold<int>(0, (sum, mark) => sum + mark));
}
output
(17)
[Mark 80, Mark 75, Mark 92]
247

The last line solves Lecture 044's puzzle. Inside print, which accepts any value, Dart inferred fold's result as Object?. Writing fold<int> tells Dart the answer directly.


12. When to Write the Type Argument

Write it when inference has nothing to go on, or picks something narrower than you need. Here map would infer List<int>; map<num> leaves room for a double later:

dart
void main() {
  var marks = [80, 75];
  var adjusted = marks.map<num>((m) => m).toList();
  adjusted.add(92.5);
  print(adjusted);
  print(adjusted.runtimeType);
}
output
[80, 75, 92.5]
List<num>

Otherwise, let Dart infer it; extra brackets that repeat the obvious are just noise.


13. Check<T> Revisited

Lecture 071's generic typedef now reads naturally: T is a type parameter, and Check<int> passes the type argument int:

dart
typedef Check<T> = bool Function(T);

void main() {
  Check<double> isHot = (temp) => temp > 35;
  print(isHot(38.5));
}
output
true

Writing your own generic classes and functions is the subject of Lecture 073 — Generic Classes & Functions.


14. A Real-World Example

Sara's shop uses a generic type for every piece of data — and never needs a cast:

dart
void main() {
  Map<String, int> prices = {'Tea': 250, 'Milk': 180, 'Bread': 120};
  List<String> order = ['Tea', 'Bread', 'Tea'];

  List<int> lineCosts = order.map<int>((item) => prices[item]!).toList();
  int total = lineCosts.fold<int>(0, (sum, cost) => sum + cost);
  Set<String> distinct = order.toSet();

  print('Items: $order');
  print('Different products: ${distinct.length}');
  print('Total: $total');
}
output
Items: [Tea, Bread, Tea]
Different products: 2
Total: 620

prices[item] is an int? (a key may be missing), so ! says it is there (Lecture 015). Every other value has a known type, so every mistake would be caught before the shop opened.


Code Examples

Example 1 — Reading Runtime Types

dart
void main() {
  var ages = [20, 18, 6];
  var temps = [21.5, 25.0];
  print(ages.runtimeType);
  print(temps.runtimeType);
}
output
List<int>
List<double>

Example 2 — A Map With Two Type Arguments

dart
void main() {
  Map<String, double> heights = {'Ali': 5.9, 'Sara': 5.4};
  heights['Zayan'] = 3.8;
  print(heights);
}
output
{Ali: 5.9, Sara: 5.4, Zayan: 3.8}

Example 3 — A Typed Empty Set

dart
void main() {
  var cities = <String>{};
  cities.add('Lahore');
  cities.add('Lahore');
  print(cities);
}
output
{Lahore}

Example 4 — Inference Finds a Shared Type

dart
void main() {
  var values = [10, 2.5, 7];
  print(values.runtimeType);
  print(values.fold<num>(0, (sum, v) => sum + v));
}
output
List<num>
19.5

Example 5 — whereType Removes the Casts

dart
void main() {
  List<Object> form = ['Ali', 20, 'Lahore', 85];
  List<String> words = form.whereType<String>().toList();
  for (final word in words) {
    print(word.toUpperCase());
  }
}
output
ALI
LAHORE

Example 6 — map Changes the Type Argument

dart
void main() {
  var names = ['Ali', 'Sara'];
  var lengths = names.map((n) => n.length).toList();
  print(lengths.runtimeType);
  print(lengths);
}
output
List<int>
[3, 4]

map turned a List<String> into a List<int>: its result type R was inferred as int.

Example 7 — Student Marks

dart
void main() {
  Map<String, List<int>> marks = {
    'Ali': [80, 75, 85],
    'Sara': [92, 88, 95],
  };
  marks.forEach((name, list) {
    int total = list.fold<int>(0, (sum, m) => sum + m);
    print('$name: $total');
  });
}
output
Ali: 240
Sara: 275

How it works

  1. The map's values are themselves List<int>, so list needs no cast.
  2. fold<int> states the result type; total is an int.

Example 8 — Shopping Total

dart
void main() {
  List<double> prices = [4, 12.5, 3.25];
  double total = prices.fold<double>(0, (sum, p) => sum + p);
  print('Prices: $prices');
  print('Total: $total');
}
output
Prices: [4.0, 12.5, 3.25]
Total: 19.75

How it works

  1. The declared List<double> turns 4 into 4.0.
  2. fold<double> makes the starting 0 a double, so + gives a double every time.

Example 9 — Bank Balance

dart
void main() {
  List<int> transactions = [1000, -250, -100, 500];
  int balance = transactions.fold<int>(0, (sum, t) => sum + t);
  List<int> withdrawals = transactions.where((t) => t < 0).toList();
  print('Balance: $balance');
  print('Withdrawals: $withdrawals');
}
output
Balance: 1150
Withdrawals: [-250, -100]

How it works

  1. where keeps the element type, so its result is a List<int> too.
  2. A stray '500' in transactions would be a compile error, not a wrong balance.

Example 10 — Temperature Labels

dart
void main() {
  List<double> temps = [18.5, 36.2, 24.0];
  List<String> labels = temps
      .map<String>((t) => t > 35 ? '$t hot' : '$t mild')
      .toList();
  print(labels);
}
output
[18.5 mild, 36.2 hot, 24.0 mild]

How it works

  1. map<String> turns each double into a String.
  2. The result is a List<String>, matching the declared type.

Common Beginner Mistakes

1. Leaving an empty collection untyped

dart
void main() {
  var orders = [];
  orders.add('Tea');
  print(orders.first.lenght); // typo, but no compile error
}
output
Unhandled exception:
NoSuchMethodError: Class 'String' has no instance getter 'lenght'.
Receiver: "Tea"
Tried calling: lenght

[] is a List<dynamic>, so the typo is found only when the program runs. Write <String>[].

2. Adding a value of the wrong type

dart
void main() {
  List<int> marks = [80];
  // marks.add('ninety'); // ✗
}
output
Error: The argument type 'String' can't be assigned to the parameter type 'int'.

3. Using List<Object> to "be flexible"

Every read then needs as, and a wrong value crashes at run time (section 5). Use the specific element type instead.

4. Giving a generic method the wrong type argument

dart
void main() {
  var prices = [4.5, 12.0];
  // print(prices.fold<int>(0, (sum, p) => sum + p)); // ✗
}
output
Error: A value of type 'double' can't be returned from a function with return type 'int'.

Adding a double to the running total gives a double. Use fold<double>.

5. Expecting List<int> to fit where List<String> is wanted

Different type arguments make different types (section 10). Convert the values with map instead.


Best Practices

  • Give every collection a specific element type; avoid List<dynamic>.
  • Type empty collections: <String>[], <String, int>{} or a declared type.
  • Prefer List<Student> over List<Object> plus as.
  • Let Dart infer type arguments when the values make them obvious.
  • Write the type argument on fold, map and whereType when inference has too little to go on or picks the wrong type.
  • Use whereType<T>() rather than a loop of casts to pick values out of a mixed list.

Mini Challenge

Zayan's teacher stores scores as List<Object> raw = [7, 'absent', 9, 8, 'late']. Without any as, build a List<int> of the real scores and print the scores and their total.

Your program should print exactly:

output
Scores: [7, 9, 8]
Total: 24

Try it yourself before opening the solution.

Show solution
dart
void main() {
  List<Object> raw = [7, 'absent', 9, 8, 'late'];
  List<int> scores = raw.whereType<int>().toList();
  int total = scores.fold<int>(0, (sum, s) => sum + s);
  print('Scores: $scores');
  print('Total: $total');
}
output
Scores: [7, 9, 8]
Total: 24

How it works

  1. whereType<int>() keeps only the int values and gives them the type int.
  2. fold<int> adds them without a single cast.

Practice Questions

Work through these in order — they run from beginner to application to challenge. Every question gives you exact values, so you can compare your output with the answers.

Beginner

  1. Create a List<String> of 'Ali', 'Sara', 'Zayan' and print its runtimeType.
  2. Create a Map<String, int> with 'Tea': 250 and 'Milk': 180, then print the price of 'Milk'.
  3. Predict the runtimeType of var values = [1, 2.0];, then check it.

Application

  1. Create an empty set of String with var, add 'Lahore' and 'Karachi', and print it.
  2. Declare List<double> temps = [20, 25]; and print it. Explain the output.
  3. From List<Object> data = [3, 'x', 4.5, 'y', 6], print only the String values as a list.
  4. Use map<String> to turn [80, 75] into ['80%', '75%'] and print it.
  5. Print the total of [1500, 2500, 1000] directly inside print, using fold with an explicit type argument.

Challenge

  1. Store Map<String, List<double>> temps = {'Lahore': [30, 34.5], 'Murree': [12, 15.5]}. Print each city with its average, as Lahore: 32.25.
  2. Start from var marks = [80, 75];. Build a List<num> copy with map, add 92.5 to it, and print the copy and its total.

Practice Answers

Question 1 — Solution

dart
void main() {
  List<String> names = ['Ali', 'Sara', 'Zayan'];
  print(names.runtimeType);
}
output
List<String>

Question 2 — Solution

dart
void main() {
  Map<String, int> prices = {'Tea': 250, 'Milk': 180};
  print(prices['Milk']);
}
output
180

Question 3 — Solution

dart
void main() {
  var values = [1, 2.0];
  print(values.runtimeType);
}
output
List<num>

int and double share the type num.

Question 4 — Solution

dart
void main() {
  var cities = <String>{};
  cities.add('Lahore');
  cities.add('Karachi');
  print(cities);
}
output
{Lahore, Karachi}

Question 5 — Solution

dart
void main() {
  List<double> temps = [20, 25];
  print(temps);
}
output
[20.0, 25.0]

The declared type makes each element a double.

Question 6 — Solution

dart
void main() {
  List<Object> data = [3, 'x', 4.5, 'y', 6];
  print(data.whereType<String>().toList());
}
output
[x, y]

Question 7 — Solution

dart
void main() {
  var marks = [80, 75];
  print(marks.map<String>((m) => '$m%').toList());
}
output
[80%, 75%]

Question 8 — Solution

dart
void main() {
  var bills = [1500, 2500, 1000];
  print(bills.fold<int>(0, (sum, b) => sum + b));
}
output
5000

Without <int>, Dart would infer Object? here and reject the +.

Question 9 — Solution

dart
void main() {
  Map<String, List<double>> temps = {
    'Lahore': [30, 34.5],
    'Murree': [12, 15.5],
  };
  temps.forEach((city, list) {
    double total = list.fold<double>(0, (sum, t) => sum + t);
    print('$city: ${total / list.length}');
  });
}
output
Lahore: 32.25
Murree: 13.75

Question 10 — Solution

dart
void main() {
  var marks = [80, 75];
  List<num> copy = marks.map<num>((m) => m).toList();
  copy.add(92.5);
  print(copy);
  print(copy.fold<num>(0, (sum, m) => sum + m));
}
output
[80, 75, 92.5]
247.5

Without map<num>, the copy would be a List<int> and add(92.5) would be rejected.


Quick Revision

  1. What does the int in List<int> tell Dart?
  2. How many type arguments does a Map take, and what are they for?
  3. What is the difference between a type parameter and a type argument?
  4. Why does reading from a List<Object> need as, and what can go wrong?
  5. What type does Dart infer for []? For [1, 2.5]?
  6. Is a List<int> a List<String>?
  7. How do you pass a type argument to fold explicitly?
  8. Name the two benefits generics bring.

Key Takeaways

  • A generic type has a slot for another type: List<int>, Map<String, int>.
  • In List<E>, E is a type parameter; in List<int>, int is the type argument.
  • Generics give safety (wrong values rejected at compile time, no casts) and reuse (one List for every type).
  • List<Object> needs casts that crash at run time; List<dynamic> hides mistakes until the program runs.
  • Dart infers type arguments from the values ([1, 2.5] → List<num>) or from the declared type; an empty literal becomes dynamic unless you type it.
  • List<int> and List<String> are different types.
  • Generic methods such as fold<T>, map<R> and whereType<T> accept an explicit type argument when inference needs help.

What's Next?

Lecture 73 — Generic Classes & Functions. So far we have only used generic types that Dart provides. Next we will write our own: a Box<T> class, a generic function like T firstOf<T>(List<T> items), and the rules for naming and using our own type parameters.


Video lecture

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