On this page22 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. What a Plain T Can Do
- 2. Adding a Bound with extends
- 3. A Bound Rejects the Wrong Type
- 4. Returning T Keeps the Exact Type
- 5. T extends Comparable<T>
- 6. Making Your Own Class Comparable
- 7. Bounding by Your Own Abstract Class
- 8. Bounded Generic Classes
- 9. Nullable vs Non-Nullable Bounds
- 10. 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 74 — Generic Constraints
| Category | 12 — Generics & Advanced Types |
| Lecture | 074 of 140 |
| Estimated video length | 10 min |
| Target | Dart 3.12.2 |
| Level | Intermediate |
A plain
Tcan be any type, so Dart lets you do almost nothing with it. WritingT extends numorT extends Shapenarrows whatTmay be, and in return you may use everything that type offers.
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 001–073:
- nullable types,
Object, andObject?(Lecture 013) compareToand comparators (Lecture 045)- abstract classes and
implements(Lectures 062–063) - type parameters, generic functions, and generic classes (Lectures 072–073)
Connection With Previous Lectures
In Lecture 073 we wrote functions and classes that work for any T. Now try something
ordinary — add up a list of numbers with a generic function:
num total<T>(List<T> values) {
num sum = 0;
for (var value in values) {
sum = sum + value;
}
return sum;
}
void main() {
print(total([1, 2, 3]));
}Error: A value of type 'T' can't be assigned to a variable of type 'num'.
sum = sum + value;
^Dart is right. Nothing stops someone calling total(['a', 'b']), and you cannot add text to
a number. We need a way to say "T can be any type — as long as it is a number".
Learning Objectives
By the end of this lecture you will be able to:
- Explain why a plain
Tallows onlyObject?members - Constrain a type parameter with
extends - Write generic
numhelpers such astotal,average, andlargest - Use
T extends Comparable<T>to find a maximum or sort any comparable type - Bound a type parameter by your own abstract class
- Read the error Dart gives when a type argument breaks a bound
- Choose between a nullable bound and
T extends Object
1. What a Plain T Can Do
Every type parameter has an upper bound — the most general type T could be. When you
write nothing, the bound is Object?: any value at all, including null.
String describe<T>(T value) => 'Value: $value';
void main() {
print(describe('Ali'));
print(describe(42));
print(describe(null));
}Value: Ali
Value: 42
Value: nullInterpolation calls toString(), which every Object? has, so this works. Ask for anything
more and Dart refuses — and its message names the bound:
double areaOf<T>(T shape) => shape.area;Error: The getter 'area' isn't defined for the type 'Object?'.Analogy — a bound is like a job advert. "Anyone may apply" means you cannot expect any skill. "Licensed drivers only" means every person hired can drive — so you can hand them the keys.
2. Adding a Bound with extends
Write extends and a type after the parameter's name. Now T must be num or a subtype
(int, double), and every num operation is allowed on it:
num total<T extends num>(List<T> values) {
num sum = 0;
for (var value in values) {
sum = sum + value;
}
return sum;
}
void main() {
print(total([1, 2, 3]));
print(total([1.5, 2.5]));
}6
4.0Here extends means "is this type or a subtype of it". It is the same word as in
inheritance, but on a type parameter it creates no class — it only sets a limit.
3. A Bound Rejects the Wrong Type
The bound is checked at every call. Text is not a num, so this call never compiles:
void main() {
print(total(['a', 'b']));
}Error: The argument type 'List<String>' can't be assigned to the parameter type 'List<num>'.
print(total(['a', 'b']));
^The constraint moved the mistake from inside your function to the caller — exactly where it belongs.
4. Returning T Keeps the Exact Type
Why not just take a List<num>? Because a function that returns T hands back the
caller's own type. Give largest a list of int and you get an int back:
T largest<T extends num>(List<T> values) {
var best = values[0];
for (var value in values) {
if (value > best) best = value;
}
return best;
}
void main() {
int top = largest([3, 9, 4]);
print(top);
print(largest([2.5, 1.5]));
}9
2.5With a num return type, int top = … would be rejected. The bound gives you >, and the
T keeps the result precise.
5. T extends Comparable<T>
compareTo (Lecture 045) is not only for numbers. String, DateTime, and any class that
implements Comparable<T> have it. Bound T by Comparable<T> and one function finds the
maximum of all of them:
T biggest<T extends Comparable<T>>(List<T> items) {
var best = items[0];
for (var item in items) {
if (item.compareTo(best) > 0) best = item;
}
return best;
}
void main() {
print(biggest(['Sara', 'Ali', 'Zayan']));
print(biggest([3, 9, 4]));
}Zayan
9Read the bound as "T must be comparable with other Ts". Without it, the call to
compareTo fails on the Object? bound:
Error: The method 'compareTo' isn't defined for the type 'Object?'.6. Making Your Own Class Comparable
Your own classes can join in. Implement Comparable<Student> and write compareTo — here,
by marks — and every Comparable helper works for students:
class Student implements Comparable<Student> {
final String name;
final int marks;
Student(this.name, this.marks);
@override
int compareTo(Student other) => marks.compareTo(other.marks);
@override
String toString() => '$name ($marks)';
}
List<T> sortedCopy<T extends Comparable<T>>(List<T> items) {
var copy = [...items];
copy.sort((a, b) => a.compareTo(b));
return copy;
}
void main() {
var students = [
Student('Ali', 72),
Student('Sara', 91),
Student('Zayan', 65),
];
print(sortedCopy(students));
print(sortedCopy(['pear', 'apple', 'fig']));
}[Zayan (65), Ali (72), Sara (91)]
[apple, fig, pear]7. Bounding by Your Own Abstract Class
A bound can be any type — including your own abstract class from Lecture 062. Every T
below is guaranteed to have name and area:
abstract class Shape {
String get name;
double get area;
}
class Square implements Shape {
final double side;
Square(this.side);
@override
String get name => 'Square';
@override
double get area => side * side;
}
class Circle implements Shape {
final double radius;
Circle(this.radius);
@override
String get name => 'Circle';
@override
double get area => 3.14 * radius * radius;
}
T largestShape<T extends Shape>(List<T> shapes) {
var best = shapes[0];
for (var shape in shapes) {
if (shape.area > best.area) best = shape;
}
return best;
}
void main() {
var circles = [Circle(1), Circle(3), Circle(2)];
var big = largestShape(circles);
print('${big.name} with radius ${big.radius}');
}Circle with radius 3.0big.radius works because largestShape returned a Circle, not just a Shape. A plain
Shape return type would have lost that.
8. Bounded Generic Classes
Bounds work on classes too. A ShapeBox<T extends Shape> can call area on whatever it
holds:
class ShapeBox<T extends Shape> {
final T shape;
ShapeBox(this.shape);
String label() => '${shape.name}: ${shape.area}';
}
void main() {
print(ShapeBox(Square(4)).label());
print(ShapeBox<Circle>(Circle(1)).label());
}Square: 16.0
Circle: 3.14(This uses Shape, Square, and Circle from section 7.) Ask for a type outside the bound
and Dart stops you:
var box = ShapeBox<String>('x');Error: Type argument 'String' doesn't conform to the bound 'Shape' of the type variable 'T' on 'ShapeBox'.
Try changing type arguments so that they conform to the bounds.9. Nullable vs Non-Nullable Bounds
The default bound Object? lets T be a nullable type. Sometimes that is wrong — a
registry key, for instance, should never be null. Bound by Object to forbid it:
String describe<T extends Object>(T value) => 'Value: $value';
void main() {
print(describe('Ali'));
int? maybe;
print(describe(maybe));
}Error: The argument type 'int?' can't be assigned to the parameter type 'Object'.
print(describe(maybe));
^The opposite direction matters too. T extends num? allows List<int?>, so T might be
null and the addition from section 2 breaks again:
num total<T extends num?>(List<T> values) {
num sum = 0;
for (var value in values) {
sum = sum + value;
}
return sum;
}Error: A value of type 'T' can't be assigned to a variable of type 'num'.| Bound | T may be null? | What you can use on T |
|---|---|---|
none (Object?) | yes | toString, ==, hashCode |
Object | no | the same, but never null |
num? | yes | only after a null check |
num | no | +, -, >, compareTo, … |
10. A Real-World Example
abstract class Product {
String get title;
double get price;
}
class Book implements Product {
@override
final String title;
@override
final double price;
final String author;
Book(this.title, this.price, this.author);
}
double cartTotal<T extends Product>(List<T> items) {
var sum = 0.0;
for (var item in items) {
sum = sum + item.price;
}
return sum;
}
T cheapest<T extends Product>(List<T> items) {
var best = items[0];
for (var item in items) {
if (item.price < best.price) best = item;
}
return best;
}
void main() {
var books = [
Book('Dart Basics', 1200, 'Ali'),
Book('Clean Code', 950, 'Sara'),
Book('Kids Coding', 600, 'Zayan'),
];
var deal = cheapest(books);
print('Total: ${cartTotal(books)}');
print('Cheapest: ${deal.title} by ${deal.author}');
}Total: 2750.0
Cheapest: Kids Coding by ZayanThe bound lets both functions read price; returning T keeps deal a Book, so
deal.author is available.
Code Examples
Example 1 — Average of Any Numbers
double average<T extends num>(List<T> values) {
num sum = 0;
for (var value in values) {
sum = sum + value;
}
return sum / values.length;
}
void main() {
print(average([80, 75, 85]));
print(average([36.5, 37.5]));
}80.0
37.0Example 2 — Smallest Number, Same Type Back
T smallest<T extends num>(List<T> values) {
var best = values[0];
for (var value in values) {
if (value < best) best = value;
}
return best;
}
void main() {
int low = smallest([40, 12, 33]);
print(low);
}12Example 3 — Clamp a Value
T clampTo<T extends num>(T value, T low, T high) {
if (value < low) return low;
if (value > high) return high;
return value;
}
void main() {
print(clampTo(120, 0, 100));
print(clampTo(-5.5, 0.0, 10.0));
}100
0.0Example 4 — Earliest Date
T smallestItem<T extends Comparable<T>>(List<T> items) {
var best = items[0];
for (var item in items) {
if (item.compareTo(best) < 0) best = item;
}
return best;
}
void main() {
var dates = [DateTime(2026, 5, 1), DateTime(2025, 12, 25)];
print(smallestItem(dates));
print(smallestItem(['Sara', 'Ali']));
}2025-12-25 00:00:00.000
AliExample 5 — Is a List in Order?
bool isSorted<T extends Comparable<T>>(List<T> items) {
for (var i = 1; i < items.length; i++) {
if (items[i - 1].compareTo(items[i]) > 0) return false;
}
return true;
}
void main() {
print(isSorted(['Ali', 'Sara', 'Zayan']));
print(isSorted(['Sara', 'Ali']));
}true
falseExample 6 — Non-Null Registry
class Registry<K extends Object> {
final List<K> _keys = [];
void add(K key) => _keys.add(key);
int get count => _keys.length;
}
void main() {
var ids = Registry<int>();
ids.add(101);
ids.add(102);
print(ids.count);
// var broken = Registry<int?>(); // ✗ breaks the Object bound
}2Example 7 — Student Leaderboard
class Student implements Comparable<Student> {
final String name;
final int marks;
Student(this.name, this.marks);
@override
int compareTo(Student other) => marks.compareTo(other.marks);
}
T biggest<T extends Comparable<T>>(List<T> items) {
var best = items[0];
for (var item in items) {
if (item.compareTo(best) > 0) best = item;
}
return best;
}
void main() {
var top = biggest([Student('Ali', 72), Student('Sara', 91)]);
print('Top: ${top.name} with ${top.marks}');
}Top: Sara with 91How it works
StudentimplementsComparable<Student>, so it fits the bound.biggestcompares withcompareTo, which compares marks.TisStudent, sotop.nameandtop.marksare available.
Example 8 — Total Wall Area
abstract class Shape {
double get area;
}
class Wall implements Shape {
final double width;
final double height;
Wall(this.width, this.height);
@override
double get area => width * height;
}
double totalArea<T extends Shape>(List<T> shapes) {
var sum = 0.0;
for (var shape in shapes) {
sum = sum + shape.area;
}
return sum;
}
void main() {
print(totalArea([Wall(4, 3), Wall(5, 3)]));
}27.0How it works
T extends Shapeguarantees every item has anareagetter.12.0 + 15.0gives27.0.- Any future
Shape— aWindow, aDoor— works without changingtotalArea.
Common Beginner Mistakes
1. Calling a method a plain T does not have
// T biggest<T>(List<T> items) => ... item.compareTo(best) ... // ✗Error: The method 'compareTo' isn't defined for the type 'Object?'.Add the bound that provides the method: T extends Comparable<T>.
2. Expecting extends to create a subclass
<T extends num> declares no class. It only limits which types may be passed as T.
3. Passing a type outside the bound
// var box = ShapeBox<String>('x'); // ✗Error: Type argument 'String' doesn't conform to the bound 'Shape' of the type variable 'T' on 'ShapeBox'.4. Using a nullable bound and then doing arithmetic
With T extends num?, the value may be null, so sum + value fails. Use T extends num
unless null really belongs in the list.
5. Bounding when you don't need to
If the function only stores or prints its values, leave T unbounded — a bound only shrinks
who can use it.
Best Practices
- Start with a plain
T; add a bound only when the body needs a member. - Choose the most general bound that works:
Comparable<T>overnumif you only compare. - Return
T, not the bound type, so callers keep their exact type. - Write
T extends Objectwhennullmust never be a valid type argument. - Bound by an abstract class to describe exactly which members
Tmust offer.
Mini Challenge
Write T pick<T extends num>(T a, T b) that returns the larger value. Then print
pick(7, 12) and pick(3.5, 2.0).
Your program should print exactly:
12
3.5Try it yourself before opening the solution.
Show solution
T pick<T extends num>(T a, T b) => a > b ? a : b;
void main() {
print(pick(7, 12));
print(pick(3.5, 2.0));
}12
3.5How it works
- The bound
nummakes>available onaandb. - The first call has
Tasint, the second asdouble. - Each call returns its own type, so
12prints without.0.
Practice Questions
Work through these in order — they run from beginner to application to challenge.
Beginner
- What is the upper bound of
Tinvoid show<T>(T value)? - Write
num doubleIt<T extends num>(T value)that returnsvalue * 2. PrintdoubleIt(21)anddoubleIt(1.5). - Will
total(['x'])compile, wheretotalisnum total<T extends num>(List<T> values)? Why or why not?
Application
- Write
T maxOfThree<T extends Comparable<T>>(T a, T b, T c)and printmaxOfThree('Ali', 'Zayan', 'Sara'). - Write
int countAbove<T extends num>(List<T> values, T limit)and printcountAbove([40, 75, 90, 55], 60). - Using
Shapewith adouble get areagetter, write a classRectand a functiondouble smallestArea<T extends Shape>(List<T> shapes). Print the result for rectangles2 × 3and1 × 4.
Challenge
- Make
class City implements Comparable<City>with anameandpopulation, comparing by population. Use a genericbiggestto print the largest of Karachi (20), Lahore (13), and Quetta (1) asKarachi. - Write a class
Pair<T extends Object>with fieldsfirstandsecondand a methodString show(). PrintPair('Ali', 'Sara').show(). Then explain in one sentence whyPair<String?>is rejected.
Practice Answers
Question 1 — Solution
Object? — T may be any type, including a nullable one.
Question 2 — Solution
num doubleIt<T extends num>(T value) => value * 2;
void main() {
print(doubleIt(21));
print(doubleIt(1.5));
}42
3.0Question 3 — Solution
No. String is not a num, so Dart reports "The argument type 'List
Question 4 — Solution
T maxOfThree<T extends Comparable<T>>(T a, T b, T c) {
var best = a;
if (b.compareTo(best) > 0) best = b;
if (c.compareTo(best) > 0) best = c;
return best;
}
void main() {
print(maxOfThree('Ali', 'Zayan', 'Sara'));
}ZayanQuestion 5 — Solution
int countAbove<T extends num>(List<T> values, T limit) {
var count = 0;
for (var value in values) {
if (value > limit) count++;
}
return count;
}
void main() {
print(countAbove([40, 75, 90, 55], 60));
}2Question 6 — Solution
abstract class Shape {
double get area;
}
class Rect implements Shape {
final double width;
final double height;
Rect(this.width, this.height);
@override
double get area => width * height;
}
double smallestArea<T extends Shape>(List<T> shapes) {
var best = shapes[0].area;
for (var shape in shapes) {
if (shape.area < best) best = shape.area;
}
return best;
}
void main() {
print(smallestArea([Rect(2, 3), Rect(1, 4)]));
}4.0Question 7 — Solution
class City implements Comparable<City> {
final String name;
final int population;
City(this.name, this.population);
@override
int compareTo(City other) => population.compareTo(other.population);
}
T biggest<T extends Comparable<T>>(List<T> items) {
var best = items[0];
for (var item in items) {
if (item.compareTo(best) > 0) best = item;
}
return best;
}
void main() {
var cities = [City('Karachi', 20), City('Lahore', 13), City('Quetta', 1)];
print(biggest(cities).name);
}KarachiQuestion 8 — Solution
class Pair<T extends Object> {
final T first;
final T second;
Pair(this.first, this.second);
String show() => '$first & $second';
}
void main() {
print(Pair('Ali', 'Sara').show());
}Ali & SaraPair<String?> is rejected because String? allows null, and the bound Object does not.
Quick Revision
- What is the default bound of a type parameter?
- What does
T extends numallow you to do with aT? - Why return
Tinstead ofnumfromlargest? - Which bound lets you call
compareToon anyT? - How do you make your own class work with
T extends Comparable<T>? - What does
T extends Objectforbid?
Key Takeaways
- Every type parameter has an upper bound; with none written, it is
Object?. - A plain
Toffers onlyObject?members, so+,compareTo, or.areafail. T extends BoundlimitsTtoBoundand its subtypes — and unlocks their members.T extends numgives arithmetic;T extends Comparable<T>gives ordering.- Your own abstract class works as a bound, for functions and for generic classes.
- A type argument outside the bound is a compile-time error at the call site.
- Return
Tto keep the caller's exact type. T extends Objectforbidsnull; a nullable bound likenum?lets it in.
What's Next?
Lecture 75 — Variance & Type Relationships. Bounds rely on one type being a subtype of
another: int fits num, Circle fits Shape. But is a List<Circle> a List<Shape>?
And can that ever go wrong at run time? Next we will study how generic types relate to each
other.