Generic Constraints and Default Type Parameters
Introduction
Unconstrained generics accept any type—which is sometimes too broad. Constraints (T extends SomeType) limit type parameters to shapes that support the operations you need. Default type parameters supply fallbacks when callers omit type arguments. Together they make generic APIs precise and ergonomic.
Prerequisites
Constraints with extends
Require that T has at least the shape of a type:
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(value: T): void {
console.log(value.length);
}
logLength("hello");
logLength([1, 2, 3]);
// logLength(42); // Error: number has no 'length'Code explanation:
T extends HasLengthmeans “any type assignable toHasLength”- Inside the function,
value.lengthis legal
Constrain to a specific key
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Ada" };
const name = getProperty(user, "name");
// const bad = getProperty(user, "age"); // ErrorCode explanation:
K extends keyof Tlimitskeyto real property names ofobj- Return type
T[K]links return type to the key
Constraining to Constructors
interface TimestampedInstance {
createdAt: Date;
}
function create<T extends new (...args: never[]) => TimestampedInstance>(
Ctor: T
): InstanceType<T> {
const instance = new Ctor();
instance.createdAt = new Date();
return instance;
}Advanced pattern—shows that constraints are not limited to object shapes. Most apps use simpler extends on interfaces.
Multiple Constraints (Intersection)
interface Printable {
print(): void;
}
interface Saveable {
save(): void;
}
function runWorkflow<T extends Printable & Saveable>(doc: T): void {
doc.print();
doc.save();
}T must satisfy both interfaces.
Default Type Parameters
Provide a default when the type argument is omitted:
Code explanation:
Pagewithout type argument usesunknownfor itemsPage<User>specializesTItem
Defaults on functions
function createState<TState extends object = Record<string, never>>(
initial: TState
): { state: TState; reset(): void } {
let state = initial;
return {
state,
reset() {
state = initial;
},
};
}Combining Constraints and Defaults
interface Entity {
id: string;
}
function findById<T extends Entity = Entity>(
items: T[],
id: string
): T | undefined {
return items.find((item) => item.id === id);
}Callers get Entity by default or a narrower type when items is more specific.
keyof Constraints in Practice
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
for (const key of keys) {
result[key] = obj[key];
}
return result;
}
const subset = pick(user, ["name"]);Pick is a built-in utility type (covered later)—here it shows why K extends keyof T matters.
When Not to Constrain
If a generic truly works for all types (identity function), leave it unconstrained:
function identity<T>(value: T): T {
return value;
}Over-constraining blocks valid uses.
Comparison Table
| Technique | Syntax | Effect |
|---|---|---|
| Unconstrained | <T> | Any type |
| Shape constraint | <T extends Foo> | Must be assignable to Foo |
| Key constraint | <K extends keyof T> | Keys of T only |
| Default | <T = Foo> | Foo when omitted |
Practical Example: Repository with Entity Constraint
Code explanation:
T extends Entityguarantees every stored item hasidRepository<Article>narrows returned items toArticle
Common Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| Constraint too narrow | Valid types rejected | Loosen interface or split overloads |
Using extends for unions incorrectly | Syntax error | Constraints use extends, not | on the parameter |
| Default incompatible with constraint | Compile error on definition | Default must satisfy extends bound |
any instead of generic | Lost safety | Introduce <T> |
FAQ
extends in generics vs class extends?
Same keyword, different jobs: generic T extends U bounds a type parameter; class extends inherits from a base class.
Can I use extends string?
Yes. T extends string allows string and string literal types—useful for branded strings in advanced patterns.
What is extends never?
Rare edge case in conditional types—not typical application code.
Order of multiple type parameters?
Defaults often appear on the rightmost parameters: <T, U = string> so callers can omit U but still specify T.
Constraint vs union on parameter?
T extends A | B is invalid as a single extends bound for a union—use T extends A or a shared base interface.
Do defaults work with inference?
Yes. Inference can still pick T while U falls back to its default when not specified.