Deeper Generics: Constraints, Defaults & Generic Classes
Make generics genuinely useful: limit them with constraints, give them default types, take several type parameters, and put them in classes and interfaces.
What you will learn
- Constrain a type parameter with extends
- Give a type parameter a default and use multiple parameters
- Write a generic class and a generic interface
From "any type" to "any *suitable* type"
A plain generic <T> accepts literally any type. That is sometimes too loose — what if your function needs the value to at least have a .length, or to be an object you can read a key from? Constraints let you say "T can be any type *as long as* it has these features". This is what turns generics from a toy into a real reusable tool.
Constraints with extends
Add extends SomeShape after the type parameter to require that T matches at least that shape. Here we only want types that have a length property:
// T can be anything, as long as it has a number length
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
console.log(longest('hello', 'hi')); // strings have length
console.log(longest([1, 2, 3], [9])); // arrays have lengthNote: Output:
hello
[ 1, 2, 3 ]
Because of T extends { length: number }, you may call .length inside the function safely. Strings and arrays both qualify, so both calls work — but longest(5, 9) (numbers have no length) would be rejected at compile time.
A constraint that links two parameters: keyof
A powerful constraint pattern reads a property from an object safely. K extends keyof T means "K must be one of T’s actual key names". This makes a get-property helper fully type-safe:
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Asha', age: 28 };
const n = getProp(user, 'name'); // typed as string
const a = getProp(user, 'age'); // typed as number
console.log(n.toUpperCase(), a + 1);
getProp(user, 'email'); // error — not a key of userNote: Output:
ASHA 29
keyof T is the union of T’s key names ('name' | 'age'). The return type T[K] is the type of that exact property, so n is a string and a is a number. Asking for 'email' — not a real key — is caught immediately.
Default type parameters
Just like a function parameter can have a default value, a type parameter can have a default type with = .... If the caller does not specify one, the default is used.
// If no type is given, T defaults to string
class Box<T = string> {
constructor(public value: T) {}
}
const a = new Box('hello'); // T inferred as string
const b = new Box<number>(42); // T explicitly number
console.log(a.value.toUpperCase());
console.log(b.value + 1);Note: Output:
HELLO
43
Box<T = string> means T is string unless you say otherwise. new Box('hello') infers string; new Box<number>(42) overrides it to number. Defaults keep simple cases tidy while allowing flexibility.
Multiple type parameters
A generic can take several type parameters — name them T, U, K, V, and so on. A pair helper needs two:
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
const p = pair('Asha', 28); // [string, number]
console.log(p[0].toUpperCase(), p[1] + 1);Note: Output:
ASHA 29
Each parameter keeps its own type: A is string, B is number, and the return tuple is [string, number]. TypeScript tracks both independently.
Generic classes and interfaces
Generics live in classes and interfaces too — this is how reusable data structures like a typed stack or a typed API response are built. The type parameter goes right after the name and is usable throughout:
// A reusable, fully typed stack of any element type
class Stack<T> {
private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
get size(): number { return this.items.length; }
}
const numbers = new Stack<number>();
numbers.push(10);
numbers.push(20);
console.log(numbers.pop()); // a number
console.log(numbers.size);Note: Output:
20
1
Stack<number> fixes T to number, so push only accepts numbers and pop returns number | undefined. Make a Stack<string> instead and the same class works for strings — one class, every type, fully checked.
A generic interface describes a shape that is parameterised by a type. A typed API envelope is the classic example:
interface ApiResponse<T> {
ok: boolean;
data: T;
}
const userResp: ApiResponse<{ name: string }> = {
ok: true,
data: { name: 'Asha' }
};
console.log(userResp.data.name);Note: Output:
Asha
ApiResponse<T> wraps any payload type in a consistent envelope. Here T is { name: string }, so userResp.data.name is fully typed. The same interface describes every endpoint’s response.
The deeper-generics toolkit
| Feature | Syntax | Why it helps |
|---|---|---|
| Constraint | <T extends Shape> | Require T to have certain features |
| Key constraint | <K extends keyof T> | Allow only real key names of T |
| Default | <T = string> | A fallback type when none is given |
| Multiple params | <A, B> | Track several types independently |
| Generic class | class Box<T> | Reusable typed data structures |
| Generic interface | interface Wrap<T> | Reusable typed shapes |
Tip: A rule of thumb: start with a plain <T>, and add extends the moment you need to *use* a feature of the value inside the function (like .length or a key). The constraint is what makes that use safe.
Watch out: A constraint narrows what callers may pass — it does not widen it. <T extends { length: number }> rejects a number even though you only read .length. Constrain to the *minimum* shape you actually need, so the generic stays as reusable as possible.
Q. What does the constraint in function longest<T extends { length: number }>(a: T, b: T) guarantee?
✍️ Practice
- Write
function firstKey<T, K extends keyof T>(obj: T, key: K): T[K]and read two different keys off an object. - Build a generic
Queue<T>class withenqueue,dequeueandsize, and use it with strings.
🏠 Homework
- Create a generic
Result<T = string>interface withok: booleanandvalue: T, plus a functionwrap<T>(value: T): Result<T>. Show it used with a number and with a default string, and note where the default applied.