Generic Constraints with extends
Without constraints, a type parameter T can be literally anything — which means TypeScript won't let you access any properties on it. Generic constraints (written with the extends keyword) narrow what T is allowed to be, giving you access to specific properties while keeping the function or class reusable.
The Problem: Unconstrained T
// Without a constraint TypeScript knows nothing about T
function getLength<T>(value: T): number {
return value.length; // ❌ Error: Property 'length' does not exist on type 'T'
}
// T could be a number, boolean, object — anything
// TypeScript can't safely allow .length accessFixing It with extends
Add extends { length: number } to tell TypeScript that T must have a length property. Now the access is safe — and the function still works for strings, arrays, and any other type with that property.
function getLength<T extends { length: number }>(value: T): number {
return value.length; // ✅ Safe — T is guaranteed to have .length
}
console.log(getLength('hello')); // 5
console.log(getLength([1, 2, 3])); // 3
console.log(getLength({ length: 7 })); // 7
// getLength(42); // ❌ number doesn't have .lengthT extends SomeType means "T must be assignable to SomeType" — not that T IS SomeType. The caller's concrete type is preserved in the return type.Constraining to an Interface
interface HasId {
id: number;
}
// Works for any object that has an id property
function findById<T extends HasId>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}
interface User { id: number; name: string; email: string }
interface Product { id: number; title: string; price: number }
const users: User[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
const products: Product[] = [
{ id: 10, title: 'Keyboard', price: 99 },
{ id: 11, title: 'Mouse', price: 49 },
];
const user = findById(users, 1); // User | undefined
const product = findById(products, 10); // Product | undefinedkeyof Constraint — Property Name Safety
Combining extends keyof T with a type parameter ensures a key argument is always a valid property of its object. This is one of the most powerful patterns in TypeScript.
// K must be a key of T
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: 'Alice', email: 'alice@example.com' };
const name = getProperty(user, 'name'); // string
const id = getProperty(user, 'id'); // number
// const bad = getProperty(user, 'age'); // ❌ 'age' is not a key of user
// Strongly typed property setter
function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
obj[key] = value;
}
setProperty(user, 'name', 'Bob'); // ✅
// setProperty(user, 'name', 42); // ❌ 42 is not assignable to stringConstraining to Primitive Types
// Only accept types that can be used as object keys
function createLookup<T, K extends string | number | symbol>(
items: T[],
getKey: (item: T) => K
): Record<K, T> {
const lookup = {} as Record<K, T>;
for (const item of items) {
lookup[getKey(item)] = item;
}
return lookup;
}
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
const byId = createLookup(users, u => u.id); // Record<number, User>
const byName = createLookup(users, u => u.name); // Record<string, User>
console.log(byId[1].name); // 'Alice'
console.log(byName['Bob'].id); // 2Multiple Constraints
TypeScript doesn't have a direct "AND" syntax for multiple extends constraints on a single type parameter, but you can use an intersection type as the constraint.
interface Printable {
print(): string;
}
interface Serializable {
serialize(): object;
}
// T must satisfy BOTH interfaces
function process<T extends Printable & Serializable>(item: T): void {
console.log(item.print());
const data = item.serialize();
console.log(JSON.stringify(data));
}
class Report implements Printable, Serializable {
constructor(private title: string, private data: unknown) {}
print(): string {
return `Report: ${this.title}`;
}
serialize(): object {
return { title: this.title, data: this.data };
}
}
process(new Report('Q4 Sales', { total: 42000 })); // ✅Constraining One Type Parameter by Another
A later type parameter can extend an earlier one. This is called a constrained type parameter and is useful for expressing relationships between types.
// K must be a key of T (K depends on T)
function pick<T, 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 user = { id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin' };
const publicInfo = pick(user, ['name', 'email']); // { name: string; email: string }
// pick(user, ['name', 'password']); // ❌ 'password' is not a key of user
// U must extend T — useful for narrowing
function coerce<T, U extends T>(value: T, guard: (v: T) => v is U): U | null {
return guard(value) ? value : null;
}
function isString(v: unknown): v is string {
return typeof v === 'string';
}
const maybeStr: unknown = 'hello';
const str = coerce(maybeStr, isString); // string | nullConstraints in Generic Classes
// A type-safe sorted collection
interface Comparable<T> {
compareTo(other: T): number; // negative, 0, or positive
}
class SortedList<T extends Comparable<T>> {
private items: T[] = [];
insert(item: T): void {
// Binary search insertion
let lo = 0, hi = this.items.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (this.items[mid].compareTo(item) < 0) lo = mid + 1;
else hi = mid;
}
this.items.splice(lo, 0, item);
}
toArray(): T[] { return [...this.items]; }
}
class Temperature implements Comparable<Temperature> {
constructor(public celsius: number) {}
compareTo(other: Temperature): number {
return this.celsius - other.celsius;
}
}
const temps = new SortedList<Temperature>();
temps.insert(new Temperature(30));
temps.insert(new Temperature(15));
temps.insert(new Temperature(22));
console.log(temps.toArray().map(t => t.celsius)); // [15, 22, 30]Conditional Constraints — Checking Type Relationships
// Only allow arrays whose elements extend a given type
function sumNumericField<T extends Record<K, number>, K extends keyof T>(
items: T[],
field: K
): number {
return items.reduce((sum, item) => sum + item[field], 0);
}
const orders = [
{ id: 1, total: 49.99, tax: 4.00 },
{ id: 2, total: 129.00, tax: 10.32 },
{ id: 3, total: 9.99, tax: 0.80 },
];
const totalRevenue = sumNumericField(orders, 'total'); // 188.98
const totalTax = sumNumericField(orders, 'tax'); // 15.12
// sumNumericField(orders, 'id'); // ✅ id is a number field
// sumNumericField(orders, 'label'); // ❌ 'label' doesn't existT extends Record<K, number> — is a common interview question and appears in real-world data-processing code. Memorise the shape; understand the mechanics.The newable Constraint
// Accept a constructor and return an instance
interface Constructor<T> {
new (...args: unknown[]): T;
}
function create<T>(Ctor: Constructor<T>): T {
return new Ctor();
}
class Logger {
log(msg: string) { console.log(msg); }
}
const logger = create(Logger); // Logger — fully typed
logger.log('created via generic factory');
// Generic mixin pattern
function Timestamped<TBase extends Constructor<object>>(Base: TBase) {
return class extends Base {
createdAt = new Date();
};
}
class User { constructor(public name: string) {} }
const TimestampedUser = Timestamped(User);
const u = new TimestampedUser('Alice');
console.log(u.name, u.createdAt); // 'Alice' <Date>Common Constraint Patterns
Constraint | Meaning | Example use |
|---|---|---|
T extends object | T must be a non-primitive | Deep clone utilities |
T extends keyof U | T must be a key of U | Property pickers |
T extends string | number | T must be a key-compatible type | Lookup tables |
T extends Partial<U> | T is a subset of U | Merge / patch helpers |
T extends new(...args) => U | T must be a constructor | Factory functions, mixins |
T extends Array<infer E> | T must be an array | Array utility functions |
Constraining Return Types
Constraints can appear on return type annotations too, tightening what a function is allowed to return.
// Ensure the returned value is a subset of the input type
function filterKeys<T extends object, K extends keyof T>(
obj: T,
keys: K[]
): Pick<T, K> {
const result = {} as Pick<T, K>;
keys.forEach(k => { result[k] = obj[k]; });
return result;
}
const user = { id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin' };
const publicInfo = filterKeys(user, ['name', 'email']);
// { name: string; email: string } — exact type, not just Partial<User>
// Constrain to make a deep-clone utility
function deepClone<T extends object>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
const clone = deepClone({ a: 1, b: { c: 2 } }); // same type as inputConstraint + Default in Practice
interface Validator<T> {
validate(value: T): boolean;
message: string;
}
// Chain validators — T is constrained to have a validate method
function composeValidators<T>(
...validators: Validator<T>[]
): Validator<T> {
return {
validate: (value: T) => validators.every(v => v.validate(value)),
message: validators.map(v => v.message).join('; '),
};
}
const isNonEmpty: Validator<string> = {
validate: (s) => s.trim().length > 0,
message: 'Must not be empty',
};
const isEmail: Validator<string> = {
validate: (s) => /^[^s@]+@[^s@]+.[^s@]+$/.test(s),
message: 'Must be a valid email',
};
const emailValidator = composeValidators(isNonEmpty, isEmail);
console.log(emailValidator.validate('alice@example.com')); // true
console.log(emailValidator.validate('invalid')); // falsePitfalls to Avoid
Over-constraining T makes the function less general — add extends only when you access properties
T extends SomeClass means 'assignable to', not 'exactly equal to' — subclasses are allowed
You cannot use extends to constrain to a literal type like T extends "GET" | "POST" in most utility scenarios; use a union parameter type instead
Circular constraints (T extends Foo<T>) are allowed but can confuse inference — test carefully
Constraints are checked at the call site, not inside the function body — TypeScript trusts that T satisfies the constraint once it is verified
Quick Reference
T extends SomeType — T must be assignable to SomeType
T extends keyof U — T must be a valid key of U
T extends A & B — T must satisfy both A and B
U extends T — U is constrained by a previous type param T
T extends new() => U — T must be a constructor returning U
Constraints enable property access: without them TypeScript refuses to access any member of T
extends are the bridge between maximum reusability and safe property access. Combined with keyof, they unlock a whole world of type-level programming. Next: default type parameters — giving generics sensible fallbacks.