5.2 Generic Constraints
Why Constraints Are Needed
Generics are powerful, but without restrictions the type parameter gives you very little to work with inside the function body.
function getLength<T>(value: T): number {
return value.length; // Error: Property 'length' does not exist on type 'T'
}
Because TypeScript does not know what T is, there is no guarantee that .length exists. A constraint lets you declare "T must be a type that has a length property."
function getLength<T extends { length: number }>(value: T): number {
return value.length; // OK
}
getLength("hello"); // 5 (string.length)
getLength([1, 2, 3]); // 3 (array.length)
getLength({ length: 10 }); // 10 (object's length)
getLength(42); // Error: number has no length
extends Constraint Basics
T extends U means "T must be a subtype of U." Every property that U has, T must also have.
// Basic extends constraint
function printName<T extends { name: string }>(item: T): void {
console.log(item.name);
}
printName({ name: "Alice" }); // OK
printName({ name: "Bob", age: 30 }); // OK (extra properties are allowed)
printName({ age: 30 }); // Error: missing name property
// Constraining with an interface
interface Serializable {
serialize(): string;
}
function saveToStorage<T extends Serializable>(item: T, key: string): void {
localStorage.setItem(key, item.serialize());
}
// Constraining with a primitive type
function add<T extends number | string>(a: T, b: T): string {
return `${a} + ${b}`;
}
add(1, 2); // OK
add("a", "b"); // OK
add(1, "b"); // Error: string is not assignable to number (T is inferred as number)
Hierarchical Constraints
// Requiring multiple properties at once
interface Identifiable {
id: number;
}
interface Timestamped {
createdAt: Date;
updatedAt: Date;
}
// Intersection type requiring both interfaces
function updateEntity<T extends Identifiable & Timestamped>(
entity: T,
updates: Partial<Omit<T, "id" | "createdAt">>
): T {
return {
...entity,
...updates,
updatedAt: new Date(),
};
}
interface User extends Identifiable, Timestamped {
name: string;
email: string;
}
const user: User = {
id: 1,
name: "Alice",
email: "alice@example.com",
createdAt: new Date("2024-01-01"),
updatedAt: new Date("2024-01-01"),
};
const updated = updateEntity(user, { name: "Alice Updated" });
keyof Constraint
keyof T returns a union of all keys of type T. K extends keyof T constrains K to "one of T's keys."
// keyof basics
interface Person {
id: number;
name: string;
age: number;
email: string;
}
type PersonKeys = keyof Person; // "id" | "name" | "age" | "email"
// K extends keyof T: safe property access
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person: Person = { id: 1, name: "Alice", age: 30, email: "alice@example.com" };
const name = getProperty(person, "name"); // string
const age = getProperty(person, "age"); // number
const id = getProperty(person, "id"); // number
getProperty(person, "phone"); // Error: 'phone' is not a key of Person
// Type-safe setter
function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): T {
return { ...obj, [key]: value };
}
const updated = setProperty(person, "name", "Bob"); // OK
const invalid = setProperty(person, "name", 42); // Error: number not assignable to string
Combining Mapped Types with keyof
// Transform all values of an object
function mapValues<T extends object, U>(
obj: T,
fn: (value: T[keyof T], key: keyof T) => U
): Record<keyof T, U> {
const result = {} as Record<keyof T, U>;
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
result[key as keyof T] = fn(obj[key as keyof T], key as keyof T);
}
}
return result;
}
const person2 = { name: "Alice", age: 30 };
const stringified = mapValues(person2, (v) => String(v));
// { name: string, age: string }
// Selectively pick keys
function pick<T extends object, K extends keyof T>(
obj: T,
keys: K[]
): Pick<T, K> {
const result = {} as Pick<T, K>;
keys.forEach((key) => {
result[key] = obj[key];
});
return result;
}
const subset = pick(person, ["name", "email"]);
// { name: string, email: string }
Combining Conditional Types with Constraints
// Precise type expressions using constraints + conditional types
type StringsOnly<T> = T extends string ? T : never;
type Test1 = StringsOnly<"hello" | 42 | "world" | boolean>;
// "hello" | "world"
// Generic with constraint + conditional type
function processIfString<T>(
value: T
): T extends string ? string : never {
if (typeof value === "string") {
return value.toUpperCase() as T extends string ? string : never;
}
throw new Error("Not a string.");
}
// Type transformation with conditional types
type Unwrap<T> = T extends Array<infer Item> ? Item : T;
type UnwrappedNumber = Unwrap<number[]>; // number
type UnwrappedString = Unwrap<string>; // string (not an array, returned as-is)
Constraint Chaining
You can combine multiple constraints to express a narrower type range.
// object constraint: excludes primitives
function cloneObject<T extends object>(obj: T): T {
return JSON.parse(JSON.stringify(obj)) as T;
}
cloneObject({ a: 1 }); // OK
cloneObject([1, 2, 3]); // OK (arrays are objects)
cloneObject(42); // Error: number is not an object
cloneObject("hello"); // Error: string is a primitive
// Record<string, unknown> constraint: guarantees index signature
function getKeys<T extends Record<string, unknown>>(obj: T): Array<keyof T> {
return Object.keys(obj) as Array<keyof T>;
}
const keys = getKeys({ a: 1, b: 2, c: 3 }); // ("a" | "b" | "c")[]
// Multiple constraints chained with &
interface HasId {
id: number;
}
interface HasName {
name: string;
}
function processItem<T extends HasId & HasName & { createdAt: Date }>(
item: T
): string {
return `[${item.id}] ${item.name} (${item.createdAt.toISOString()})`;
}
// Cross-parameter constraint: one type parameter constrains another
function copyFrom<Target extends Source, Source>(
source: Source,
factory: () => Target
): Target {
return Object.assign(factory(), source);
}
Combining Record with Constraints
// Processing objects with dynamic keys
function transformRecord<
K extends string,
V,
R
>(
record: Record<K, V>,
transform: (value: V, key: K) => R
): Record<K, R> {
const result = {} as Record<K, R>;
for (const key in record) {
if (Object.prototype.hasOwnProperty.call(record, key)) {
result[key] = transform(record[key], key);
}
}
return result;
}
const prices = { apple: 1.5, banana: 0.5, cherry: 3.0 };
const discounted = transformRecord(prices, (price) => price * 0.9);
// { apple: number, banana: number, cherry: number }
extends vs implements: The Difference
In TypeScript generics you can only use extends for constraints. implements is reserved for class declarations.
// Correct constraint syntax
interface Printable {
print(): void;
}
// implements is used only in class declarations
class Document implements Printable {
print(): void {
console.log("Printing document");
}
}
// Generic constraints always use extends (even for interfaces)
function printAll<T extends Printable>(items: T[]): void {
items.forEach((item) => item.print());
}
// T extends Printable means "T is any type that structurally satisfies Printable"
// It doesn't have to be a class instance — structural compatibility is enough
const obj = {
print() { console.log("Anonymous object printing"); }
};
printAll([obj]); // OK — structurally satisfies Printable
printAll([new Document()]); // OK — Document implements Printable
// Abstract class constraint
abstract class Animal {
abstract speak(): string;
move(): void {
console.log(`Moving while ${this.speak()}`);
}
}
// Abstract classes are also constrained with extends
function makeNoise<T extends Animal>(animal: T): string {
return animal.speak();
}
class Dog extends Animal {
speak(): string { return "Woof"; }
}
class Cat extends Animal {
speak(): string { return "Meow"; }
}
makeNoise(new Dog()); // "Woof"
makeNoise(new Cat()); // "Meow"
Practical Examples
getProperty — Type-Safe Property Access
// Safe access for nested objects (one level)
function getProperty<T extends object, K extends keyof T>(
obj: T,
key: K
): T[K] {
return obj[key];
}
// Safe access for nested objects (two levels — includes type inference)
function getNestedProperty<
T extends object,
K1 extends keyof T,
K2 extends keyof T[K1]
>(obj: T, key1: K1, key2: K2): T[K1][K2] {
return (obj[key1] as T[K1])[key2];
}
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
server: {
host: string;
port: number;
};
}
const config: Config = {
database: {
host: "localhost",
port: 5432,
credentials: {
username: "admin",
password: "secret",
},
},
server: {
host: "0.0.0.0",
port: 8080,
},
};
const dbHost = getNestedProperty(config, "database", "host"); // string
const serverPort = getNestedProperty(config, "server", "port"); // number
Sortable Array
// Guarantee sortability with a Comparable constraint
interface Comparable<T> {
compareTo(other: T): number;
}
function sortArray<T extends Comparable<T>>(arr: T[]): T[] {
return [...arr].sort((a, b) => a.compareTo(b));
}
class Temperature implements Comparable<Temperature> {
constructor(private celsius: number) {}
compareTo(other: Temperature): number {
return this.celsius - other.celsius;
}
toString(): string {
return `${this.celsius}°C`;
}
}
const temperatures = [
new Temperature(30),
new Temperature(15),
new Temperature(25),
new Temperature(5),
];
const sorted = sortArray(temperatures);
sorted.forEach((t) => console.log(t.toString()));
// 5°C, 15°C, 25°C, 30°C
// Sorting primitives — type-safe with generics + extends
function sortPrimitive<T extends string | number | bigint>(arr: T[]): T[] {
return [...arr].sort((a, b) => {
if (a < b) return -1;
if (a > b) return 1;
return 0;
});
}
sortPrimitive([3, 1, 4, 1, 5, 9]); // number[]
sortPrimitive(["banana", "apple", "cherry"]); // string[]
sortPrimitive([{}, {}]); // Error: object is not string | number | bigint
Deep Object Access (Combined with Optional Chaining)
// Type for safe deep path access
type DeepGet<T, Path extends string> =
Path extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? DeepGet<T[Key], Rest>
: never
: Path extends keyof T
? T[Path]
: never;
// Practical alternative: combining runtime safety with type safety
function safeGet<T extends object>(
obj: T,
path: string,
defaultValue?: unknown
): unknown {
const keys = path.split(".");
let current: unknown = obj;
for (const key of keys) {
if (current === null || current === undefined) {
return defaultValue;
}
if (typeof current !== "object") {
return defaultValue;
}
current = (current as Record<string, unknown>)[key];
}
return current ?? defaultValue;
}
const deepObj = {
user: {
profile: {
name: "Alice",
address: {
city: "New York",
},
},
},
};
console.log(safeGet(deepObj, "user.profile.name")); // "Alice"
console.log(safeGet(deepObj, "user.profile.address.city")); // "New York"
console.log(safeGet(deepObj, "user.missing.key", "N/A")); // "N/A"
Pro Tips
Balancing Constraint Strictness and Reusability
// Too strict — low reusability
function processUser<T extends {
id: number;
name: string;
email: string;
age: number;
role: "admin" | "user";
}>(user: T): string {
return `${user.name} (${user.email})`;
}
// Problem: only name and email are needed, but far too many properties required
// Appropriate constraint — require only what you need
function processUser2<T extends { name: string; email: string }>(user: T): string {
return `${user.name} (${user.email})`;
}
// Any object with name and email works
// Too loose — loss of type safety
function processAny<T>(item: T): string {
return String(item); // No information about structure — can't do much beyond toString
}
// Balanced: require only the properties actually used
function formatEntity<T extends { id: number | string; toString(): string }>(
entity: T
): string {
return `[${entity.id}] ${entity.toString()}`;
}
Precise Type Inference with Conditional Types and Constraints
// Array → element type, otherwise pass through
type ElementOf<T> = T extends (infer E)[] ? E : T;
function first<T>(collection: T): ElementOf<T> {
if (Array.isArray(collection)) {
return collection[0] as ElementOf<T>;
}
return collection as ElementOf<T>;
}
const n = first([1, 2, 3]); // number
const s = first("hello"); // string (not an array)
// Filter keys by value type in an object
type KeysWithValueType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never
}[keyof T];
interface Mixed {
id: number;
name: string;
active: boolean;
score: number;
label: string;
}
type StringKeys = KeysWithValueType<Mixed, string>; // "name" | "label"
type NumberKeys = KeysWithValueType<Mixed, number>; // "id" | "score"
function getStringProp<T extends object>(
obj: T,
key: KeysWithValueType<T, string>
): string {
return obj[key] as string;
}
const mixed: Mixed = { id: 1, name: "Alice", active: true, score: 95, label: "A" };
getStringProp(mixed, "name"); // OK
getStringProp(mixed, "label"); // OK
getStringProp(mixed, "id"); // Error: id is a number
Minimizing Type Parameter Propagation
// Bad pattern: unnecessarily many type parameters
function badMerge<
T extends object,
U extends object,
K1 extends keyof T,
K2 extends keyof U
>(obj1: T, obj2: U, key1: K1, key2: K2): T[K1] & U[K2] {
return { ...(obj1[key1] as object), ...(obj2[key2] as object) } as T[K1] & U[K2];
}
// Good pattern: only the constraints that are necessary
function goodMerge<T extends object, U extends object>(
obj1: T,
obj2: U
): T & U {
return { ...obj1, ...obj2 };
}
Summary Table
| Constraint Pattern | Syntax | Meaning |
|---|---|---|
| Basic extends | T extends U | T is a subtype of U |
| Property constraint | T extends { name: string } | T has a name property |
| keyof constraint | K extends keyof T | K is one of T's keys |
| object constraint | T extends object | T is not a primitive |
| Compound constraint | T extends A & B | T satisfies both A and B |
| Cross-parameter constraint | U extends T | U is a subtype of T |
| Record constraint | T extends Record<string, unknown> | T has an index signature |
| Array constraint | T extends unknown[] | T is an array type |
Up Next...
Section 5.3 covers Built-in Utility Types. You will explore the utility types TypeScript ships with — Partial, Required, Readonly, Pick, Omit, Record, and more — implement each one yourself to understand how they work internally, and learn how to combine them for complex DTO transformation patterns and a custom DeepPartial.