Generic Constraints & Defaults
Reviewed & published by Brayan K
A generic constraint, written <T extends Constraint>, restricts a type parameter to types that have a required shape so the function body can use those members safely, while a default type parameter, <T = SomeType>, supplies a fallback type when the caller doesn't provide or imply one.
Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Restrict a type parameter with <T extends Constraint>
- Use the constrained members safely inside the function body
- Provide a fallback type with default parameters <T = string>
- Declare and constrain multiple type parameters at once
- Constrain a key to keyof T for a type-safe getter
- Read property types precisely with the lookup type T[K]
💡 Real-World Analogy
An unconstrained generic is a job posting that says "anyone may apply". You can't ask applicants to do anything specific because you don't know what they can do. A constraint is a job requirement — "must have a driving licence" (extends { length: number }). Now you can confidently ask every applicant to drive (read .length), and anyone without the licence is rejected at the door (a compile error). A default type is the "if unspecified, we'll assume X" line on the form — a sensible fallback so the common case needs no extra paperwork.
1. <T extends Constraint>
A bare <T> could be anything, so you can't touch any of its properties. Adding extends a shape promises those members exist. The function stays generic — it accepts strings, arrays, or any object with a length — yet the body can safely read .length:
// Constrain T to anything with a numeric length
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("hello", "hi"); // ✅ strings have length -> string
longest([1, 2, 3], [9]); // ✅ arrays have length -> number[]
longest(10, 20); // ❌ number has no 'length' property
// Returning T (not the constraint) preserves the caller's exact type
function logLength<T extends { length: number }>(item: T): T {
console.log(item.length);
return item; // caller still gets the precise type back
}// === Generic constraints: <T extends Constraint> ===
// A constraint promises that T has certain members, so the function body
// can safely use them. In TS:
// function longest<T extends { length: number }>(a: T, b: T): T { ... }
// The constraint "{ length: number }" guarantees .length exists.
function longest(a, b) {
return a.length >= b.length ? a : b; // safe: both have .length
}
console.log(longest("hello", "hi")); // hello
console.log(longest([1, 2, 3], [9])); // [ 1, 2, 3 ]
console.log(longest("ab", "abcd")); // abcd
// Without the constraint, .length might not exist (e.g. on a number),
// and TypeScript would refuse to compile. The constraint is what makes
// the property access provably safe.
// Constraints also let us return a precise type. Here logLength returns
// the same value it was given, so the caller keeps the exact type:
function logLength(item) {
console.log("length is", item.length);
return item;
}
const back = logLength([10, 20]); // length is 2
console.log("got back:", back); // got back: [ 10, 20 ]2. Default Parameters & Multiple Constraints
A default type parameter <T = string> is used when the caller neither specifies T explicitly nor gives an argument TypeScript can infer it from. You can also declare several type parameters, each with its own constraint, and combine constraints with defaults:
// Default type parameter — falls back to string
function makeBox<T = string>(value: T): { value: T } {
return { value };
}
makeBox(); // T defaults; { value: string }
makeBox(42); // inferred: { value: number }
// Multiple constrained parameters — both must be objects
function merge<A extends object, B extends object>(a: A, b: B): A & B {
return { ...a, ...b };
}
const m = merge({ name: "Alice" }, { age: 30 });
// type of m is { name: string } & { age: number }
// Constraint AND default together
function wrap<T extends object = {}>(x: T): { data: T } {
return { data: x };
}// === Default type parameters: <T = string> ===
// A generic can have a DEFAULT type, used when the caller doesn't specify
// (and TypeScript can't infer) one. In TS:
// function makeBox<T = string>(value: T): { value: T } { ... }
// Runtime analogue: a normal default value when no argument is passed.
function makeBox(value = "default") { // mirrors <T = string>
return { value };
}
console.log(makeBox()); // { value: 'default' } (used the default)
console.log(makeBox(42)); // { value: 42 }
console.log(makeBox(true)); // { value: true }
// Multiple constrained params — a "merge" that keeps BOTH shapes.
// In TS: function merge<A extends object, B extends object>(a: A, b: B): A & B
function merge(a, b) {
return { ...a, ...b }; // result has all keys of both
}
const merged = merge({ name: "Alice" }, { age: 30 });
console.log("merged:", merged); // merged: { name: 'Alice', age: 30 }
console.log("keys:", Object.keys(merged)); // keys: [ 'name', 'age' ]3. Constraining to keyof — a Safe Getter
The most useful constraint pairs two parameters: <T, K extends keyof T>. The second parameter is forced to be a real key of the first, so a typo becomes a compile error and the return type T[K] is exactly the type of that property:
// K must be one of T's real keys; return type is the property's type
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Alice", admin: true };
getProp(user, "name"); // ✅ typed as string -> "Alice"
getProp(user, "id"); // ✅ typed as number -> 1
getProp(user, "nope"); // ❌ '"nope"' is not assignable to keyof user
// A matching setter — the value must fit the property's type
function setProp<T, K extends keyof T>(obj: T, key: K, value: T[K]): T {
obj[key] = value;
return obj;
}// === Constraining to keyof — a type-safe property getter ===
// The classic example: getProp(obj, key) returns obj[key], and the constraint
// <T, K extends keyof T> guarantees key is a REAL key of obj.
// At runtime, that's a safe indexed read:
function getProp(obj, key) {
return obj[key];
}
const user = { id: 1, name: "Alice", admin: true };
console.log(getProp(user, "name")); // Alice
console.log(getProp(user, "id")); // 1
console.log(getProp(user, "admin")); // true
// In TypeScript, getProp(user, "nope") would FAIL to compile because
// "nope" is not assignable to keyof typeof user. The constraint K extends
// keyof T turns a typo into a compile error instead of a runtime undefined.
// A setter version, same idea — key must be a real key:
function setProp(obj, key, value) {
obj[key] = value;
return obj;
}
const updated = setProp({ ...user }, "name", "Bob");
console.log("after set:", updated.name); // after set: Bob🎯 Your Turn
Implement the body of a function whose generic is constrained to anything with a length. Fill in the blanks and match the expected output.
// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// Mirror a constrained generic:
// function firstItem<T extends { length: number }>(list: T) { ... }
// The constraint guarantees list has .length and is indexable.
function firstItem(list) {
// 1) If the list has at least one item, return the first one...
return list.length ___ 0 ? ___ : "empty"; // 👉 > 0 and list[0]
}
console.log(firstItem([10, 20, 30])); // 10
console.log(firstItem("hello")); // h (strings have length too!)
console.log(firstItem([])); // empty
// ✅ Expected output:
// 10
// h
// emptyCommon Errors (and the fix)
- ❌ "Property 'x' does not exist on type 'T'": you accessed a member on an unconstrained generic. ✅ Add a constraint that promises it: <T extends { x: ... }>.
- ❌ Passing a bad key: getProp(user, "naem") → "Argument of type '"naem"' is not assignable to parameter of type 'keyof ...'". ✅ Fix the typo; the keyof constraint caught it for you.
- ❌ Constraint too loose: <T extends object> then doing arithmetic — object doesn't have +. ✅ Constrain to exactly what you use ({ value: number }).
- ❌ Default never applies: <T = string> seems ignored because an argument let TypeScript infer a different T. ✅ Defaults only kick in when T can't be inferred or specified.
- ❌ Out-of-order parameters: <K extends keyof T, T> errors because K references T before it's declared. ✅ Declare T first: <T, K extends keyof T>.
Pro Tips
- 💡 Constrain to exactly the members you use — no looser, no tighter. It keeps the function maximally reusable yet still safe.
- 💡 Return T, not the constraint, so callers keep their precise types.
- 💡 <T, K extends keyof T> is the workhorse pattern behind type-safe getters, setters, and column selectors — memorise it.
- 💡 Combine constraint + default (<T extends object = {}>) when a type must be restricted but also has a sensible no-argument fallback.
Mini-Challenge: pluck
Build a type-safe pluck that extracts one property from every object in an array — the runtime form of <T, K extends keyof T>. Follow the outline, run it, and match the example output.
// 🎯 MINI-CHALLENGE: a type-safe "pluck"
// In TS this constrains the key to be a real property of the items:
// function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] { ... }
//
// Write pluck(items, key) that:
// 1. Takes an array of objects and a key name.
// 2. Returns a new array of just that property from each object.
// (Hint: items.map((item) => item[key]))
//
// Test data:
// const people = [
// { name: "Alice", age: 30 },
// { name: "Bob", age: 25 },
// { name: "Carol", age: 41 },
// ];
// pluck(people, "name") and pluck(people, "age")
//
// ✅ Example output:
// [ 'Alice', 'Bob', 'Carol' ]
// [ 30, 25, 41 ]
// your code here🎉 Lesson Complete
- ✅ <T extends Constraint> limits a type parameter so its members are safe to use
- ✅ Return T to preserve the caller's exact type
- ✅ <T = SomeType> supplies a fallback type when T can't be inferred
- ✅ Declare multiple constrained parameters, e.g. <A extends object, B extends object>
- ✅ <T, K extends keyof T> + return T[K] builds a type-safe getter
- ✅ A parameter can be constrained by another parameter declared before it
Practice quiz
What does '<T extends { length: number }>' promise inside the function body?
- T is always a string
- T is an array
- T has a numeric 'length' property you can safely use
- T is unknown
Answer: T has a numeric 'length' property you can safely use. The constraint guarantees the member exists, so reading a.length is provably safe.
Why can't you read a property off an unconstrained '<T>'?
- T could be anything (e.g. a number), so no property is provably present
- Generics are slow
- TypeScript forbids all generics
- It needs a default
Answer: T could be anything (e.g. a number), so no property is provably present. Without a constraint, the compiler knows nothing about T, so member access is rejected.
What is the difference between a constraint and a default type parameter?
- They are the same
- A default limits T; a constraint sets a fallback
- Both supply fallbacks
- A constraint LIMITS what T can be; a default supplies a FALLBACK when T is not given/inferred
Answer: A constraint LIMITS what T can be; a default supplies a FALLBACK when T is not given/inferred. <T extends U> restricts T; <T = U> provides a fallback only when T is not specified or inferred.
What does 'keyof T' produce?
- The values of T
- The union of all of T's property names
- A new object
- T[K]
Answer: The union of all of T's property names. keyof T is the union of T's property name literals.
In 'function getProp<T, K extends keyof T>(obj: T, key: K)', what is the return type?
- T[K] (the property's type)
- any
- T
- K
Answer: T[K] (the property's type). The lookup type T[K] is exactly the type of the selected property.
What does the K extends keyof T constraint catch at compile time?
- Slow code
- Missing return statements
- A bad/typo key like getProp(user, "naem")
- Circular types
Answer: A bad/typo key like getProp(user, "naem"). Passing a key that is not a real property of T becomes a compile error, not a runtime undefined.
When does a default type parameter '<T = string>' actually apply?
- Always
- Only when T is neither specified explicitly nor inferable from an argument
- Only with keyof
- Never
Answer: Only when T is neither specified explicitly nor inferable from an argument. If TypeScript can infer T from an argument, the inferred type wins over the default.
What is the return type of 'merge<A extends object, B extends object>(a: A, b: B)'?
- object
- A | B
- unknown
- A & B
Answer: A & B. The intersection A & B has all properties of both inputs, keeping the merged object fully typed.
Why declare 'T' before 'K' in '<T, K extends keyof T>'?
- Alphabetical order
- A parameter can only be constrained by ones declared before it
- K is faster
- It is optional
Answer: A parameter can only be constrained by ones declared before it. <K extends keyof T, T> errors because K references T before it is declared.
How do you both restrict a type parameter and give it a fallback?
- <T = object extends {}>
- <T: object | {}>
- <T extends object = {}>
- You cannot combine them
Answer: <T extends object = {}>. <T extends object = {}> means T must be an object and defaults to {} when not provided.
Continue this course
- Previous: Conditional Types & infer
- Next: keyof, typeof & Lookup Types — Type-safe property access with keyof, the typeof operator, and T[K]
- Quick reference: TypeScript cheat sheet › Functions & Generics
Frequently asked questions
Why constrain a generic instead of just using a plain type parameter?
An unconstrained <T> is a black box — TypeScript knows nothing about it, so you can't access any property safely. The moment you write a.length inside the function, the compiler complains because a generic T might be a number. Adding <T extends { length: number }> tells the compiler 'whatever T turns out to be, it definitely has a numeric length', which both unlocks the property access inside the body and rejects callers who pass something without it. Constraints are how you keep a function generic while still using the parts of the value you need.
What's the difference between a constraint and a default type parameter?
A constraint (<T extends U>) LIMITS what T can be — callers must pass a type assignable to U. A default (<T = U>) supplies a FALLBACK type used only when T isn't given and can't be inferred. They are independent and often combined: <T extends object = {}> means 'T must be an object, and if you don't specify one, assume the empty object'. Think of constraint as the rule and default as the no-argument behaviour.
How does <K extends keyof T> make a property getter type-safe?
keyof T is the union of all of T's property names. Constraining K to extend keyof T forces the key argument to be one of those real names, so getProp(user, 'naem') is a compile error rather than a silent undefined at runtime. The return type T[K] (a lookup type) is then exactly the type of that property — so getProp(user, 'age') is typed as number, not any. This pattern is the foundation of every type-safe accessor, form helper, and ORM column selector.
Can one type parameter's constraint reference another?
Yes, and it's extremely common. In function getProp<T, K extends keyof T>, the constraint on K depends on T. TypeScript resolves them together: it first works out T from the object argument, then checks K against that specific T's keys. You can chain further — for example <T, K extends keyof T, V extends T[K]> — to constrain a value to match the type of the selected property. Order matters: a parameter can only be constrained by ones declared before it.