TypeScript Generics Constraints
A reference guide to TypeScript generic constraints using extends, keyof, constructor signatures, and intersections.
TL;DR
- Constrain type parameters using the
extendskeyword with contracts. - Limit generic access to valid property keys with
keyofindexing. - Combine multiple constraint requirements using the intersection
&operator.
Structural Constraints
extends object shapeRequires an argument to possess a minimum set of properties.
interface HasId { id: string; }
function printId<T extends HasId>(item: T): string {
return item.id;
}length constraintRestricts parameters to strings, arrays, or countable objects.
function getLength<T extends { length: number }>(
val: T
): number {
return val.length;
}primitive union limitLimits generics strictly to scalar string or number types.
function identity<T extends string | number>(v: T): T {
return v;
}Keyof Index Constraints
keyof property lookupSafely reads dynamic object fields without type assertions.
function getProp<T, K extends keyof T>(
obj: T,
key: K
): T[K] {
return obj[key];
}property setter helperConstrains updated values to the target property type.
function setProp<T, K extends keyof T>(
obj: T,
key: K,
val: T[K]
): void {
obj[key] = val;
}multiple key selectionExtracts subsets of properties using array key constraints.
function pluck<T, K extends keyof T>(
items: T[],
key: K
): T[K][] {
return items.map(item => item[key]);
}Multiple Intersected Constraints
intersected contractsRequires satisfying multiple contracts simultaneously.
interface Named { name: string; }
interface Aged { age: number; }
function verify<T extends Named & Aged>(
val: T
): string {
return `${val.name} is ${val.age}`;
}class and interface mergeEnsures an entity extends a base class and satisfies traits.
class Model {}
interface Syncable { sync(): void; }
function save<T extends Model & Syncable>(m: T) {
m.sync();
}record constraintEnforces dictionary structure while narrowing value types.
function logMap<T extends Record<string, number>>(
dict: T
): void {
for (const [k, v] of Object.entries(dict)) {
console.log(`${k}: ${v}`);
}
}Constructor Constraints
newable constructorConstrains arguments to valid instantiable class types.
type Constructor<T> = new (...args: any[]) => T;
function createInstance<T>(ctor: Constructor<T>): T {
return new ctor();
}factory with argumentsPasses constructor arguments through generic factory helpers.
function createWithArgs<T, A extends any[]>(
ctor: new (...args: A) => T,
...args: A
): T {
return new ctor(...args);
}abstract class constructorReferences abstract constructor signatures in mixin helpers.
type AbstractConstructor<T> = abstract new (
...args: any[]
) => T;Conditional Constraints
nested conditional limitsRefines types based on generic constraint evaluations.
type Unpack<T> = T extends (infer U)[] ? U : T;
type Num = Unpack<number[]>; // numberfiltering never constraintExcludes incompatible union members using never.
type NonEmpty<T> = T extends '' | null | undefined
? never
: T;infer in constraintExtracts inner component types through pattern matching.
type ItemType<T> = T extends { items: (infer I)[] }
? I
: never;Tips
- Use
K extends keyof Tto guarantee that property lookup helpers return precise property values without casting. - Apply constructor signatures like
new (...args: any[]) => Twhen authoring generic factory methods or dependency injection containers.
Warnings
- Avoid over-constraining
<T>with excessive properties because it limits caller flexibility and clutters function signatures unnecessarily. - Remember that
extendsconstraints enforce minimum structural requirements rather than exact nominal matches, permitting excess properties at runtime.
In Practice
Demonstrates keyof constraints, generic return typing, and safe record lookups in a data transformation helper.
- Declare a generic function accepting an arbitrary object and a constrained key.
- Use K extends keyof T to restrict the property argument to known object keys.
- Return the exact index access type T[K] to preserve full type fidelity.
- Invoke the helper and verify that non-existent keys trigger compile errors.
function getProperty<T, K extends keyof T>(
entity: T,
propertyKey: K
): T[K] {
return entity[propertyKey];
}
const user = {
id: 'usr_42',
username: 'pat_dev',
reputation: 1580
};
const rep = getProperty(user, 'reputation'); // number
const uname = getProperty(user, 'username'); // string
console.log(`User ${uname} has ${rep} points.`);FAQ
Use the extends keyword followed by the required object structure, such as . This guarantees that callers can only supply arguments containing an id property, granting the function safe access to that property.
keyof T represents the union of all accessible key names on type T. Writing K extends keyof T declares a separate type parameter K constrained to that key union, enabling TypeScript to preserve the exact property value type T[K].
Specify a newable call signature in the constraint, such as . This tells the compiler that the parameter can be invoked with the new keyword to create an instance of type T.
function instantiate<T>(ctor: new () => T): T {
return new ctor();
}Yes, you can combine multiple interface contracts into a single constraint using an intersection type: . Any argument passed to the function must satisfy both interfaces simultaneously.