Skip to main content

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 PatternSyntaxMeaning
Basic extendsT extends UT is a subtype of U
Property constraintT extends { name: string }T has a name property
keyof constraintK extends keyof TK is one of T's keys
object constraintT extends objectT is not a primitive
Compound constraintT extends A & BT satisfies both A and B
Cross-parameter constraintU extends TU is a subtype of T
Record constraintT extends Record<string, unknown>T has an index signature
Array constraintT 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.