On this page26 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. What Are Generics?
- 2. Reading the Angle Brackets
- 3. Type Parameters Are Placeholders
- 4. The Problem Without Generics: List<Object>
- 5. Casts Turn Compile Errors into Crashes
- 6. List<dynamic> Is Even Worse
- 7. Safety and Reuse — Both at Once
- 8. Type Inference for Generic Types
- 9. The Declared Type Fills the Slot Too
- 10. Different Type Arguments, Different Types
- 11. Generic Methods You Already Used
- 12. When to Write the Type Argument
- 13. Check<T> Revisited
- 14. A Real-World Example
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 72 — Generics Fundamentals
| Category | 12 — Generics & Advanced Types |
| Lecture | 072 of 140 |
| Estimated video length | 12 min |
| Target | Dart 3.12.2 |
| Level | Intermediate |
A list that accepts anything can't promise anything, so every read needs a risky cast. Generics let one
Listserve 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:
isandas, and the difference betweenObjectanddynamic- lists, sets and maps, including
List<Object>and the emptyList<dynamic> map,where,whereType<T>()andfold- 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:
void main() {
var marks = [80, 75];
// print(marks.fold(0, (sum, mark) => sum + mark)); // ✗
}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>andMap<String, int> - Distinguish a type parameter from a type argument
- Explain why
List<Object>andList<dynamic>lead to casts and run-time crashes - Predict the type Dart infers for a list, set or map literal
- Recognize that
List<int>andList<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.
void main() {
List<int> marks = [80, 75, 92];
List<String> names = ['Ali', 'Sara', 'Zayan'];
print(marks.runtimeType);
print(names.runtimeType);
}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":
| Type | Read 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:
void main() {
Map<String, int> marks = {'Ali': 80, 'Sara': 92};
marks['Zayan'] = 70;
print(marks['Sara']);
print(marks);
}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:
void main() {
List<int> marks = [80, 75];
marks.add(92);
// marks.add('ninety'); // ✗
print(marks);
}[80, 75, 92]The commented line is rejected before the program runs:
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:
void main() {
List<Object> marks = [80, 75, 92];
var total = 0;
for (final mark in marks) {
// total = total + mark; // ✗
}
print(total);
}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:
void main() {
List<Object> marks = [80, 75, 'ninety'];
var total = 0;
for (final mark in marks) {
total = total + (mark as int);
}
print(total);
}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:
void main() {
List<dynamic> names = ['Ali', 'Sara', 42];
for (final name in names) {
print(name.toUpperCase());
}
}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:
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
}
}Ali: 80
Sara: 92Dart 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):
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);
}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:
void main() {
List<double> prices = [4, 12];
List<String> orders = [];
var basket = <String>[];
print(prices);
print(orders.runtimeType);
print(basket.runtimeType);
}[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:
void main() {
List<int> marks = [80, 75];
print(marks is List<String>);
// List<String> names = marks; // ✗
}falseError: 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:
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));
}(17)
[Mark 80, Mark 75, Mark 92]
247The 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:
void main() {
var marks = [80, 75];
var adjusted = marks.map<num>((m) => m).toList();
adjusted.add(92.5);
print(adjusted);
print(adjusted.runtimeType);
}[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:
typedef Check<T> = bool Function(T);
void main() {
Check<double> isHot = (temp) => temp > 35;
print(isHot(38.5));
}trueWriting 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:
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');
}Items: [Tea, Bread, Tea]
Different products: 2
Total: 620prices[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
void main() {
var ages = [20, 18, 6];
var temps = [21.5, 25.0];
print(ages.runtimeType);
print(temps.runtimeType);
}List<int>
List<double>Example 2 — A Map With Two Type Arguments
void main() {
Map<String, double> heights = {'Ali': 5.9, 'Sara': 5.4};
heights['Zayan'] = 3.8;
print(heights);
}{Ali: 5.9, Sara: 5.4, Zayan: 3.8}Example 3 — A Typed Empty Set
void main() {
var cities = <String>{};
cities.add('Lahore');
cities.add('Lahore');
print(cities);
}{Lahore}Example 4 — Inference Finds a Shared Type
void main() {
var values = [10, 2.5, 7];
print(values.runtimeType);
print(values.fold<num>(0, (sum, v) => sum + v));
}List<num>
19.5Example 5 — whereType Removes the Casts
void main() {
List<Object> form = ['Ali', 20, 'Lahore', 85];
List<String> words = form.whereType<String>().toList();
for (final word in words) {
print(word.toUpperCase());
}
}ALI
LAHOREExample 6 — map Changes the Type Argument
void main() {
var names = ['Ali', 'Sara'];
var lengths = names.map((n) => n.length).toList();
print(lengths.runtimeType);
print(lengths);
}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
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');
});
}Ali: 240
Sara: 275How it works
- The map's values are themselves
List<int>, solistneeds no cast. fold<int>states the result type;totalis anint.
Example 8 — Shopping Total
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');
}Prices: [4.0, 12.5, 3.25]
Total: 19.75How it works
- The declared
List<double>turns4into4.0. fold<double>makes the starting0adouble, so+gives adoubleevery time.
Example 9 — Bank Balance
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');
}Balance: 1150
Withdrawals: [-250, -100]How it works
wherekeeps the element type, so its result is aList<int>too.- A stray
'500'intransactionswould be a compile error, not a wrong balance.
Example 10 — Temperature Labels
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);
}[18.5 mild, 36.2 hot, 24.0 mild]How it works
map<String>turns eachdoubleinto aString.- The result is a
List<String>, matching the declared type.
Common Beginner Mistakes
1. Leaving an empty collection untyped
void main() {
var orders = [];
orders.add('Tea');
print(orders.first.lenght); // typo, but no compile error
}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
void main() {
List<int> marks = [80];
// marks.add('ninety'); // ✗
}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
void main() {
var prices = [4.5, 12.0];
// print(prices.fold<int>(0, (sum, p) => sum + p)); // ✗
}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>overList<Object>plusas. - Let Dart infer type arguments when the values make them obvious.
- Write the type argument on
fold,mapandwhereTypewhen 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:
Scores: [7, 9, 8]
Total: 24Try it yourself before opening the solution.
Show solution
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');
}Scores: [7, 9, 8]
Total: 24How it works
whereType<int>()keeps only theintvalues and gives them the typeint.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
- Create a
List<String>of'Ali','Sara','Zayan'and print itsruntimeType. - Create a
Map<String, int>with'Tea': 250and'Milk': 180, then print the price of'Milk'. - Predict the
runtimeTypeofvar values = [1, 2.0];, then check it.
Application
- Create an empty set of
Stringwithvar, add'Lahore'and'Karachi', and print it. - Declare
List<double> temps = [20, 25];and print it. Explain the output. - From
List<Object> data = [3, 'x', 4.5, 'y', 6], print only theStringvalues as a list. - Use
map<String>to turn[80, 75]into['80%', '75%']and print it. - Print the total of
[1500, 2500, 1000]directly insideprint, usingfoldwith an explicit type argument.
Challenge
- Store
Map<String, List<double>> temps = {'Lahore': [30, 34.5], 'Murree': [12, 15.5]}. Print each city with its average, asLahore: 32.25. - Start from
var marks = [80, 75];. Build aList<num>copy withmap, add92.5to it, and print the copy and its total.
Practice Answers
Question 1 — Solution
void main() {
List<String> names = ['Ali', 'Sara', 'Zayan'];
print(names.runtimeType);
}List<String>Question 2 — Solution
void main() {
Map<String, int> prices = {'Tea': 250, 'Milk': 180};
print(prices['Milk']);
}180Question 3 — Solution
void main() {
var values = [1, 2.0];
print(values.runtimeType);
}List<num>int and double share the type num.
Question 4 — Solution
void main() {
var cities = <String>{};
cities.add('Lahore');
cities.add('Karachi');
print(cities);
}{Lahore, Karachi}Question 5 — Solution
void main() {
List<double> temps = [20, 25];
print(temps);
}[20.0, 25.0]The declared type makes each element a double.
Question 6 — Solution
void main() {
List<Object> data = [3, 'x', 4.5, 'y', 6];
print(data.whereType<String>().toList());
}[x, y]Question 7 — Solution
void main() {
var marks = [80, 75];
print(marks.map<String>((m) => '$m%').toList());
}[80%, 75%]Question 8 — Solution
void main() {
var bills = [1500, 2500, 1000];
print(bills.fold<int>(0, (sum, b) => sum + b));
}5000Without <int>, Dart would infer Object? here and reject the +.
Question 9 — Solution
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}');
});
}Lahore: 32.25
Murree: 13.75Question 10 — Solution
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));
}[80, 75, 92.5]
247.5Without map<num>, the copy would be a List<int> and add(92.5) would be rejected.
Quick Revision
- What does the
intinList<int>tell Dart? - How many type arguments does a
Maptake, and what are they for? - What is the difference between a type parameter and a type argument?
- Why does reading from a
List<Object>needas, and what can go wrong? - What type does Dart infer for
[]? For[1, 2.5]? - Is a
List<int>aList<String>? - How do you pass a type argument to
foldexplicitly? - 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>,Eis a type parameter; inList<int>,intis the type argument. - Generics give safety (wrong values rejected at compile time, no casts) and reuse
(one
Listfor 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 becomesdynamicunless you type it. List<int>andList<String>are different types.- Generic methods such as
fold<T>,map<R>andwhereType<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.