You have been using Dart generics for weeks without calling them that. Every time you wrote List<String> or Map<String, int>, those angle brackets were generics doing their quiet job. Then you want to write your own container, a little class that holds one value and hands it back, and you want it to work with an int today and a String tomorrow. You could reach for Object and cast everything, but then the compiler stops helping you and the bugs move to runtime. Dart generics are the better tool: they let you write one class or one function that works with many types while the compiler still checks every line. By the end of this post you will be able to write a generic class, use a type parameter, add a bound with extends, and write a generic method that infers its own type.
What the angle brackets actually mean
A generic is a class or function that takes a type as a kind of argument. Instead of hard-coding int or String into your class, you leave a blank, a type parameter, and fill it in when you use the class. By convention that blank is a single capital letter, usually T for “type.” Here is the smallest generic worth showing, a box that holds one value of whatever type you give it.
class Box<T> {
T value;
Box(this.value);
T get() => value;
}
void main() {
var intBox = Box<int>(42);
var strBox = Box<String>('hello');
print(intBox.get()); // 42
print(strBox.get()); // hello
}
The <T> after the class name is the whole trick. It tells Dart that Box is not finished until someone says what T is. Inside the class, T stands in for that type everywhere: the value field is a T, and get() returns a T. Down in main, Box<int>(42) fills the blank with int, so for that object value is an int. The next line fills the same blank with String, a completely separate kind of box. One class, two types, and you never had to write IntBox and StringBox by hand. Run it and you get 42 then hello, each value the exact type you put in.
A type parameter is a blank you fill in with a real type when you build the object.
Why Dart generics beat Object and casting
The natural question is why bother, when you could type value as Object and store anything in it. Every class in Dart is an Object, so that field really will hold anything. The problem shows up the moment you take the value back out and try to use it.
class LooseBox {
Object value;
LooseBox(this.value);
}
void main() {
var box = LooseBox(42);
String s = box.value; // does not compile
}
Dart refuses to run this, with a message you should learn to read:
A value of type 'Object' can't be assigned to a variable of type 'String'.
The compiler is right to complain. As far as it knows, box.value is just some Object, and most objects are not strings. To compile it you would have to cast with box.value as String. But as moves the check to runtime: get it wrong and the program compiles fine, then crashes when a user hits that line. Generics give you the same flexibility without the risk. Because Box<int> remembers that T is int, the compiler knows the exact type at every step and will not let you put the wrong thing in.
var intBox = Box<int>(42);
intBox.value = 'oops'; // does not compile
That line fails before the program ever runs, with A value of type 'String' can't be assigned to a variable of type 'int'. That is the trade worth understanding: Object gives you flexibility and takes away the compiler’s help, while generics give you the flexibility and keep the help. In CIS225 this is usually the moment students stop reaching for casts and let the type parameter carry the information instead.

Generic methods: type safety for a single function
You do not need a whole class to use generics. A single function can have its own type parameter, and this is where you will use them most often. Say you want a function that returns the first item of any list as the right type. Without generics you would return Object and be stuck casting again. With a type parameter, the type flows straight through.
T firstOf<T>(List<T> items) => items.first;
void main() {
var nums = [10, 20, 30];
var words = ['ada', 'grace'];
int n = firstOf(nums); // 10
String w = firstOf(words); // ada
print(n); // 10
print(w); // ada
}
Read the signature slowly. firstOf<T> declares a type parameter, takes a List<T>, and returns a single T. Call firstOf(nums) where nums is a List<int>, and Dart works out that T is int for this call, so the result is an int and lands cleanly in n. Pass a list of strings and T becomes String instead. Notice you never wrote firstOf<int>(nums), though you could. Dart infers the type from the argument, the same inference that lets you write var, so you get a specific return type without the noise of declaring it.
A type parameter on a function lets one function keep the exact type of whatever you pass it.
Bounding a type parameter with extends
Plain T means “absolutely any type,” which is powerful but limiting: all Dart knows about a bare T is that it is some object, so you cannot call anything type-specific on it. The fix is a bound. You use extends in the angle brackets to say “T can be any type, as long as it is at least this kind of thing.” Suppose you want a function that finds the largest item in a list. To compare two items you need a type that knows how to compare itself, which in Dart means it implements Comparable.
T largest<T extends Comparable<T>>(List<T> items) {
var max = items.first;
for (var item in items) {
if (item.compareTo(max) > 0) max = item;
}
return max;
}
void main() {
print(largest([3, 9, 2])); // 9
print(largest(['pear', 'apple', 'fig'])); // pear
}
The bound <T extends Comparable<T>> is doing the heavy lifting. It promises Dart that whatever T turns out to be, it has a compareTo method, so item.compareTo(max) is allowed. Numbers and strings both implement Comparable, so both calls work: the largest of 3, 9, 2 is 9, and the largest of the three words is pear, because compareTo sorts strings alphabetically and p comes after a and f. Call largest on a class that does not implement Comparable and Dart stops you at compile time with The argument type 'List<Widget>' can't be assigned to the parameter type 'List<Comparable<Object?>>'. That error is the bound protecting you: it will not let you ask for the largest of things that have no notion of order.

When to reach for generics, and when to skip them
Generics are satisfying once they click, which is why it is worth knowing when not to use them. The rule is the same one that governs inheritance and mixins: add the machinery when the problem asks for it, not before. Reach for a generic when you are writing something that should work with many types while staying type-safe, a container, a cache, a utility function, code that does not care what it holds but does care that you get the same type back. That is why Dart’s own collections are all generic: every List<T>, Set<T>, and Map<K, V> is a generic class, which is why a List<String> hands you strings and never makes you cast.
But if a class or function only ever works with one concrete type, leave the type parameter out. A Money class that always holds a double does not need to be Money<T>, and dressing it up in generics just adds noise for the next person to decode. The quick test I give students is to ask whether the type really varies. If you can name the one type it will always be, write that type. If you catch yourself reaching for Object and a cast so one piece of code handles several types, that is the signal a generic belongs there instead.
Use a generic when the type truly varies, and a plain type when it does not.
Your next step
Here is the short version worth keeping. A generic lets one class or function work with many types by leaving a type parameter, usually T, as a blank you fill in when you use it. That beats typing a field as Object, because generics keep the exact type and the compiler keeps checking, while Object and as push your errors to runtime. Functions infer their type parameters from the arguments you pass, and when you need to call methods on a type parameter, bound it with extends so Dart knows what the type can do.
The way to make this stick is to build one. Write a generic Stack<T> with a private List<T> inside, a push method that adds an item, a pop that removes and returns the last one, and an isEmpty getter. Then make a Stack<String>, push two words, pop one, and watch the type stay a String the whole way through without a cast. Once that feels natural, the Dart classes post is worth a second read, since a generic class is just a class with a blank in it, and the Dart sets post shows those same angle brackets on a collection you already use. The Dart abstract classes post pairs well too, since generics and abstract types often appear together. For the full rules, the Dart language guide on generics lays out every wrinkle. Open your editor and give one class a blank to fill in.

