Watch the video (10 min)
On this page22 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What a Plain T Can Do
  5. 2. Adding a Bound with extends
  6. 3. A Bound Rejects the Wrong Type
  7. 4. Returning T Keeps the Exact Type
  8. 5. T extends Comparable<T>
  9. 6. Making Your Own Class Comparable
  10. 7. Bounding by Your Own Abstract Class
  11. 8. Bounded Generic Classes
  12. 9. Nullable vs Non-Nullable Bounds
  13. 10. A Real-World Example
  14. Code Examples
  15. Common Beginner Mistakes
  16. Best Practices
  17. Mini Challenge
  18. Practice Questions
  19. Practice Answers
  20. Quick Revision
  21. Key Takeaways
  22. What's Next?

Lecture 74 — Generic Constraints

Category12 — Generics & Advanced Types
Lecture074 of 140
Estimated video length10 min
TargetDart 3.12.2
LevelIntermediate

A plain T can be any type, so Dart lets you do almost nothing with it. Writing T extends num or T extends Shape narrows what T may 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, and Object? (Lecture 013)
  • compareTo and 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:

dart
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]));
}
output
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 T allows only Object? members
  • Constrain a type parameter with extends
  • Write generic num helpers such as total, average, and largest
  • 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.

dart
String describe<T>(T value) => 'Value: $value';

void main() {
  print(describe('Ali'));
  print(describe(42));
  print(describe(null));
}
output
Value: Ali
Value: 42
Value: null

Interpolation calls toString(), which every Object? has, so this works. Ask for anything more and Dart refuses — and its message names the bound:

dart
double areaOf<T>(T shape) => shape.area;
output
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:

dart
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]));
}
output
6
4.0

Here 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:

dart
void main() {
  print(total(['a', 'b']));
}
output
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:

dart
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]));
}
output
9
2.5

With 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:

dart
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]));
}
output
Zayan
9

Read the bound as "T must be comparable with other Ts". Without it, the call to compareTo fails on the Object? bound:

output
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:

dart
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']));
}
output
[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:

dart
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}');
}
output
Circle with radius 3.0

big.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:

dart
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());
}
output
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:

dart
var box = ShapeBox<String>('x');
output
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:

dart
String describe<T extends Object>(T value) => 'Value: $value';

void main() {
  print(describe('Ali'));
  int? maybe;
  print(describe(maybe));
}
output
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:

dart
num total<T extends num?>(List<T> values) {
  num sum = 0;
  for (var value in values) {
    sum = sum + value;
  }
  return sum;
}
output
Error: A value of type 'T' can't be assigned to a variable of type 'num'.
BoundT may be null?What you can use on T
none (Object?)yestoString, ==, hashCode
Objectnothe same, but never null
num?yesonly after a null check
numno+, -, >, compareTo, …

10. A Real-World Example

dart
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}');
}
output
Total: 2750.0
Cheapest: Kids Coding by Zayan

The 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

dart
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]));
}
output
80.0
37.0

Example 2 — Smallest Number, Same Type Back

dart
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);
}
output
12

Example 3 — Clamp a Value

dart
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));
}
output
100
0.0

Example 4 — Earliest Date

dart
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']));
}
output
2025-12-25 00:00:00.000
Ali

Example 5 — Is a List in Order?

dart
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']));
}
output
true
false

Example 6 — Non-Null Registry

dart
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
}
output
2

Example 7 — Student Leaderboard

dart
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}');
}
output
Top: Sara with 91

How it works

  1. Student implements Comparable<Student>, so it fits the bound.
  2. biggest compares with compareTo, which compares marks.
  3. T is Student, so top.name and top.marks are available.

Example 8 — Total Wall Area

dart
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)]));
}
output
27.0

How it works

  1. T extends Shape guarantees every item has an area getter.
  2. 12.0 + 15.0 gives 27.0.
  3. Any future Shape — a Window, a Door — works without changing totalArea.

Common Beginner Mistakes

1. Calling a method a plain T does not have

dart
// T biggest<T>(List<T> items) => ... item.compareTo(best) ... // ✗
output
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

dart
// var box = ShapeBox<String>('x'); // ✗
output
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> over num if you only compare.
  • Return T, not the bound type, so callers keep their exact type.
  • Write T extends Object when null must never be a valid type argument.
  • Bound by an abstract class to describe exactly which members T must 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:

output
12
3.5

Try it yourself before opening the solution.

Show solution
dart
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));
}
output
12
3.5

How it works

  1. The bound num makes > available on a and b.
  2. The first call has T as int, the second as double.
  3. Each call returns its own type, so 12 prints without .0.

Practice Questions

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

Beginner

  1. What is the upper bound of T in void show<T>(T value)?
  2. Write num doubleIt<T extends num>(T value) that returns value * 2. Print doubleIt(21) and doubleIt(1.5).
  3. Will total(['x']) compile, where total is num total<T extends num>(List<T> values)? Why or why not?

Application

  1. Write T maxOfThree<T extends Comparable<T>>(T a, T b, T c) and print maxOfThree('Ali', 'Zayan', 'Sara').
  2. Write int countAbove<T extends num>(List<T> values, T limit) and print countAbove([40, 75, 90, 55], 60).
  3. Using Shape with a double get area getter, write a class Rect and a function double smallestArea<T extends Shape>(List<T> shapes). Print the result for rectangles 2 × 3 and 1 × 4.

Challenge

  1. Make class City implements Comparable<City> with a name and population, comparing by population. Use a generic biggest to print the largest of Karachi (20), Lahore (13), and Quetta (1) as Karachi.
  2. Write a class Pair<T extends Object> with fields first and second and a method String show(). Print Pair('Ali', 'Sara').show(). Then explain in one sentence why Pair<String?> is rejected.

Practice Answers

Question 1 — Solution

Object? — T may be any type, including a nullable one.

Question 2 — Solution

dart
num doubleIt<T extends num>(T value) => value * 2;

void main() {
  print(doubleIt(21));
  print(doubleIt(1.5));
}
output
42
3.0

Question 3 — Solution

No. String is not a num, so Dart reports "The argument type 'List' can't be assigned to the parameter type 'List'."

Question 4 — Solution

dart
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'));
}
output
Zayan

Question 5 — Solution

dart
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));
}
output
2

Question 6 — Solution

dart
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)]));
}
output
4.0

Question 7 — Solution

dart
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);
}
output
Karachi

Question 8 — Solution

dart
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());
}
output
Ali & Sara

Pair<String?> is rejected because String? allows null, and the bound Object does not.


Quick Revision

  1. What is the default bound of a type parameter?
  2. What does T extends num allow you to do with a T?
  3. Why return T instead of num from largest?
  4. Which bound lets you call compareTo on any T?
  5. How do you make your own class work with T extends Comparable<T>?
  6. What does T extends Object forbid?

Key Takeaways

  • Every type parameter has an upper bound; with none written, it is Object?.
  • A plain T offers only Object? members, so +, compareTo, or .area fail.
  • T extends Bound limits T to Bound and its subtypes — and unlocks their members.
  • T extends num gives 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 T to keep the caller's exact type.
  • T extends Object forbids null; a nullable bound like num? 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.


Video lecture

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