TypeScript 27 🔷 Generic Constraints
A generic constraint narrows what types a type parameter can be. Without a constraint, T can be anything — which means the function or class can’t assume anything about it. With T extends SomeShape, the type parameter must be assignable to SomeShape, and the code can safely use the members of that shape. Constraints are what turn a generic from “works with any type” into “works with any type that has what I need.” Almost every useful generic function has one.
Key point: A constraint is written T extends U, but it doesn’t mean inheritance — it means T must be assignable to U. Once constrained, T has all of U‘s members inside the function. The constraint is a requirement on the caller, checked at compile time. Without it, you can’t access anything on T; with it, you can access everything the constraint provides.
Why constraints exist
Consider a function that needs to read a property from its argument.
Without a constraint:
function getLength<T>(x: T): number {
return x.length; // ❌ Property 'length' does not exist on type 'T'
}
TypeScript rejects this. T could be number, boolean, or anything without a length. The compiler can’t allow the access.
With a constraint:
function getLength<T extends { length: number }>(x: T): number {
return x.length; // ✅ T is guaranteed to have length
}
The constraint says “T must have a length property of type number.” Inside the function, x.length is safe. The compiler allows it.
What changed: The constraint narrows the set of types T can be. Instead of “any type,” it’s “any type with length: number.” The function now knows enough to do something useful.
The error is real: Without the constraint, getLength(42) would be allowed (with T = number), and the function would crash at runtime. TypeScript prevents the mistake by requiring the constraint.
Why constraints are essential: They’re the tool that makes generics useful. Without them, you can only pass values around, not operate on them. With them, you can access properties, call methods, and use values in type-safe ways. Every non-trivial generic function uses at least one constraint — it’s how you tell the compiler “I need
Tto have this much.”
Basic constraint syntax
A constraint is written with extends in the type parameter list.
function f<T extends SomeType>(x: T): void { }
Simple shape constraint:
function printName<T extends { name: string }>(obj: T): void {
console.log(obj.name);
}
printName({ name: 'Alice', age: 30 }); // ✅
printName({ name: 'Bob' }); // ✅
printName({ id: 1 }); // ❌ no name property
The constraint requires name: string. Any object with that property satisfies it — extra properties are fine.
Class constraint:
class Animal {
name = '';
}
function describe<T extends Animal>(a: T): string {
return `Animal named ${a.name}`;
}
describe(new Animal()); // ✅
describe({ name: 'Rex' }); // ✅ structurally compatible
describe({ id: 1 }); // ❌ missing name
T extends Animal requires T to have Animal‘s shape. Classes and structurally compatible objects both work.
Union constraint:
function stringify<T extends string | number>(x: T): string {
return String(x);
}
stringify('hello'); // ✅
stringify(42); // ✅
stringify(true); // ❌ boolean not allowed
T can be string or number — nothing else.
Built-in constraint:
function keys<T extends object>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[];
}
keys({ a: 1, b: 2 }); // ('a' | 'b')[]
keys([1, 2, 3]); // ✅ array is an object
T extends object requires T to be an object type — not a primitive.
Constraint with a type parameter:
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
K is constrained to keyof T — the keys of T. This is one of the most useful constraints.
Why
extendsand not something else:extendsin a constraint means “is assignable to.” It’s the same keyword as class inheritance but with a different meaning in this position. TypeScript’s designers reused it because the relationship — “T is a subset of U” — is analogous to a subclass being a subset of its parent. The meaning is consistent enough to be intuitive.
Constraining to an interface
One of the most common constraints is to an interface.
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
const users = [{ id: 'u-1', name: 'Alice' }];
findById(users, 'u-1'); // ✅
findById([{ name: 'x' }], 'u-1'); // ❌ missing id
HasId describes the minimum shape T must have. The function uses item.id safely.
Why this pattern is common: Many operations need one identifying property — an ID, a key, a name. The interface captures that requirement, and the constraint enforces it. Any type with an id: string works.
Constraining to multiple interfaces: Combine with an intersection.
interface HasId { id: string; }
interface HasName { name: string; }
function describe<T extends HasId & HasName>(item: T): string {
return `${item.name} (${item.id})`;
}
describe({ id: '1', name: 'Alice' }); // ✅
describe({ id: '1' }); // ❌ missing name
describe({ name: 'Alice' }); // ❌ missing id
T must have both — an intersection of shapes.
Constraining to an abstract class:
abstract class Entity {
abstract id: string;
describe(): string { return `Entity ${this.id}`; }
}
function logEntity<T extends Entity>(entity: T): void {
console.log(entity.describe());
}
The constraint requires the full Entity shape. Subclasses and structural matches both work.
Why interface constraints are idiomatic: They express the minimum requirement precisely.
T extends HasIdsays “T must have an ID.” The consumer doesn’t care about other properties — any shape with an ID works. That’s the structural typing advantage applied to generics.
The keyof constraint
K extends keyof T is one of the most powerful constraints in TypeScript. It says “K must be a key of T.”
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: 'Alice', email: 'alice@example.com' };
get(user, 'id'); // number
get(user, 'name'); // string
get(user, 'email'); // string
get(user, 'missing'); // ❌ not a key of user
K is restricted to the actual keys of T. The return type T[K] is the type of that property. TypeScript knows get(user, 'id') is a number and get(user, 'name') is a string.
Why this works: keyof T produces a union of the keys. K extends keyof T restricts K to that union. When you call get(user, 'id'), TypeScript infers K = 'id' and resolves T[K] to number.
Property assignment:
function set<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
obj[key] = value;
}
set(user, 'name', 'Alice'); // ✅
set(user, 'name', 42); // ❌ must be string
set(user, 'missing', 'x'); // ❌ not a key
The value parameter is typed T[K], so setting name requires a string. The compiler catches type mismatches.
Picking properties:
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 picked = pick(user, ['id', 'name']);
// { id: number; name: string }
Pick<T, K> uses the keys to produce a type with only those properties.
Array of keys:
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map(item => item[key]);
}
pluck(users, 'name'); // string[]
pluck(users, 'id'); // number[]
The return type is an array of the property’s type.
Why keyof is so useful: It ties one type parameter to the keys of another. Any operation that “selects a property by name” uses it — get, set, pick, omit, pluck. Without keyof, you’d use string, lose the type relationship, and allow typos.
Why
K extends keyof Tis idiomatic: It’s the standard pattern for “the key must actually be a key of the object.” The compiler checks it at the call site. Typos fail, wrong types fail, and the return type is precise. That combination is what makes TypeScript’s object manipulation feel safe.
Constraint to a constructor type
A constraint can require T to be “newable” — a class or constructor function.
type Constructor<T = object> = new (...args: any[]) => T;
function create<T extends Constructor>(Ctor: T): InstanceType<T> {
return new Ctor();
}
T extends Constructor requires T to be a constructor. The return type InstanceType<T> extracts the instance type.
Usage:
class User {
name = 'Alice';
}
const u = create(User); // User
TypeScript infers T = typeof User and resolves InstanceType<T> to User.
With arguments:
function createWith<T extends new (name: string) => object>(
Ctor: T,
name: string
): InstanceType<T> {
return new Ctor(name);
}
class Product {
constructor(public name: string) {}
}
const p = createWith(Product, 'Keyboard'); // Product
T is constrained to a constructor that takes a string. InstanceType<T> gives the right instance type.
Constraining a factory:
function register<T extends Constructor>(
registry: Map<string, T>,
name: string,
Ctor: T
): void {
registry.set(name, Ctor);
}
register accepts any constructor. The registry stores it. Later code can create instances.
Why constructor constraints matter: They’re how you type factories, dependency injection, decorators, and mixins. The constraint requires a “newable” thing; InstanceType<T> gives the produced type. Without this, factory functions would return object or any.
Why
InstanceTypeexists: A constructor type and an instance type are different.typeof Useris the constructor;Useris what instances are.InstanceType<T>bridges them — given a constructor type, it gives the instance type. Factory functions use it to return the correct type.
Multiple constraints
You can constrain a type parameter to multiple things with an intersection, or constrain several parameters together.
Intersection constraint:
function save<T extends HasId & Serializable>(item: T): void {
const json = item.serialize();
console.log(`Saving ${item.id}: ${json}`);
}
T must have both id and serialize.
Multiple constrained parameters:
function merge<T extends object, U extends object>(
a: T,
b: U
): T & U {
return { ...a, ...b };
}
Both T and U must be objects. The return is the intersection.
One parameter constrained by another:
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
K is constrained by T. The relationship is between parameters, not to a fixed type.
Chained constraints:
function setProperty<T, K extends keyof T, V extends T[K]>(
obj: T,
key: K,
value: V
): void {
obj[key] = value;
}
V is constrained to T[K] — the type of the property. So value must be assignable to that property’s type.
Why chained constraints: They express relationships between multiple type parameters. K extends keyof T and V extends T[K] capture “K is a key of T, V is that key’s type.” The constraints make the relationships explicit and checked.
Why intersections for multiple constraints: There’s no syntax for “T extends A and B” in a single clause.
T extends A & Buses the intersection type, which is the shape that has both. It’s the same mechanism as combining interfaces elsewhere.
Default type parameters with constraints
A constraint can have a default, applied when the type isn’t inferred or specified.
function create<T extends object = object>(): T {
return {} as T;
}
const a = create(); // T = object
const b = create<{ id: number }>(); // T = { id: number }
The default object applies when T isn’t given. The constraint T extends object still holds.
Default referencing another parameter:
class Result<T, E extends Error = Error> {
constructor(
public ok: boolean,
public value?: T,
public error?: E
) {}
}
const r1: Result<number> = new Result(true, 42); // E = Error
const r2: Result<number, TypeError> = new Result(false, undefined, new TypeError('x'));
E defaults to Error, and the constraint requires it to be an Error subclass.
Default with a shape:
interface Options {
timeout?: number;
retries?: number;
}
function fetch<T extends Options = Options>(): T {
return {} as T;
}
Default is the constraint itself.
Why defaults matter: They let the function be called without specifying all type parameters. Result<T> is convenient because most code doesn’t care about the error type — it just wants the value type. The default E = Error covers the common case.
Why a default is often the constraint: When
Tisn’t inferrable and isn’t specified, it makes sense to fall back to the constraint. That’s the least-surprising choice — the caller gets the most general type that still satisfies the requirement.E = Errorfollows the same logic.
A full example
A type-safe data layer with multiple constraints.
// ============================================
// SHAPES
// ============================================
interface Entity {
readonly id: string;
}
interface Timestamped {
readonly createdAt: Date;
updatedAt: Date;
}
interface Serializable {
serialize(): string;
}
// ============================================
// GENERIC FUNCTIONS WITH CONSTRAINTS
// ============================================
// Constraint: must have id
function findById<T extends Entity>(
items: T[],
id: string
): T | undefined {
return items.find(item => item.id === id);
}
// Constraint: must be object, K must be a key
function pluck<T, K extends keyof T>(
items: T[],
key: K
): T[K][] {
return items.map(item => item[key]);
}
// Constraint: multiple shapes
function touch<T extends Entity & Timestamped>(item: T): T {
item.updatedAt = new Date();
return item;
}
// Constraint: constructor
function create<T extends new () => object>(Ctor: T): InstanceType<T> {
return new Ctor();
}
// Constraint: index with a key
function indexBy<T extends Entity>(items: T[]): Map<string, T> {
const map = new Map<string, T>();
for (const item of items) {
map.set(item.id, item);
}
return map;
}
// ============================================
// DOMAIN
// ============================================
interface User extends Entity, Timestamped, Serializable {
name: string;
email: string;
serialize(): string;
}
class Product implements Entity, Timestamped, Serializable {
readonly id: string;
readonly createdAt = new Date();
updatedAt = new Date();
constructor(public name: string, public price: number) {
this.id = crypto.randomUUID();
}
serialize(): string {
return JSON.stringify(this);
}
}
// ============================================
// USAGE
// ============================================
const users: User[] = [
{
id: 'u-1',
name: 'Alice',
email: 'alice@example.com',
createdAt: new Date(),
updatedAt: new Date(),
serialize() { return JSON.stringify(this); }
},
{
id: 'u-2',
name: 'Bob',
email: 'bob@example.com',
createdAt: new Date(),
updatedAt: new Date(),
serialize() { return JSON.stringify(this); }
}
];
const alice = findById(users, 'u-1');
// User | undefined
const names = pluck(users, 'name');
// string[]
const emails = pluck(users, 'email');
// string[]
const touched = touch(users[0]);
// User — updatedAt refreshed
const indexed = indexBy(users);
// Map<string, User>
const product = create(Product);
// Product
console.log(alice?.name);
console.log(names, emails);
console.log(indexed.get('u-2')?.name);
console.log(product.name);
What this shows:
Entityconstraint —findById,indexBykeyofconstraint —pluck- Multiple constraints —
touchrequiresEntity & Timestamped - Constructor constraint —
create
Every function is generic, and every constraint enforces what the function needs.
Why this shape: It’s a realistic data-access module. Constraints document what each function requires.
findByIdneeds an ID;pluckneeds a valid key;touchneeds both an entity and a timestamp;createneeds a constructor. The compiler checks all of them.
Complete Example Session
# ============================================
# PART 1: BASIC CONSTRAINT
# ============================================
cat > basic.ts << 'EOF'
function getLength<T extends { length: number }>(x: T): number {
return x.length;
}
console.log(getLength('hello')); // 5
console.log(getLength([1, 2, 3])); // 3
console.log(getLength({ length: 42 })); // 42
// getLength(42); // ❌
EOF
npx tsc --noEmit basic.ts
# (no errors)
# ============================================
# PART 2: INTERFACE CONSTRAINT
# ============================================
cat > iface.ts << 'EOF'
interface HasId { id: string; }
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
const users = [
{ id: 'u-1', name: 'Alice' },
{ id: 'u-2', name: 'Bob' }
];
console.log(findById(users, 'u-1'));
EOF
npx tsc --noEmit iface.ts
# (no errors)
# ============================================
# PART 3: KEYOF CONSTRAINT
# ============================================
cat > keyof.ts << 'EOF'
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: 'Alice', active: true };
const n = get(user, 'name'); // string
const i = get(user, 'id'); // number
const a = get(user, 'active'); // boolean
console.log(n, i, a);
// get(user, 'missing'); // ❌
EOF
npx tsc --noEmit keyof.ts
# (no errors)
# ============================================
# PART 4: MULTIPLE CONSTRAINTS
# ============================================
cat > multi.ts << 'EOF'
interface HasId { id: string; }
interface HasName { name: string; }
function describe<T extends HasId & HasName>(x: T): string {
return `${x.name} (${x.id})`;
}
console.log(describe({ id: '1', name: 'Alice' }));
// describe({ id: '1' }); // ❌
EOF
npx tsc --noEmit multi.ts
# (no errors)
# ============================================
# PART 5: CONSTRUCTOR CONSTRAINT
# ============================================
cat > ctor.ts << 'EOF'
function create<T extends new () => object>(Ctor: T): InstanceType<T> {
return new Ctor();
}
class User {
name = 'Alice';
}
const u = create(User);
console.log(u.name);
EOF
npx tsc --noEmit ctor.ts
# (no errors)
# ============================================
# PART 6: TRIGGER ERRORS
# ============================================
cat > errors.ts << 'EOF'
interface HasId { id: string; }
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
// ❌ Missing id
findById([{ name: 'x' }], 'x');
EOF
npx tsc --noEmit errors.ts
# [ errors.ts:9:1 - Type '{ name: string; }[]' is not assignable to parameter of type 'HasId[]'. ]
# [ errors.ts:9:1 - Property 'id' is missing in type '{ name: string; }' but required in type 'HasId'. ]
rm errors.ts
# ============================================
# PART 7: COMPILE AND RUN
# ============================================
npx tsc basic.ts iface.ts keyof.ts multi.ts ctor.ts
node basic.js
# [ 5 ]
# [ 3 ]
# [ 42 ]
node iface.js
# [ { id: 'u-1', name: 'Alice' } ]
node keyof.js
# [ Alice 1 true ]
node multi.js
# [ Alice (1) ]
node ctor.js
# [ Alice ]
Quick Reference
Constraint Syntax
| Form | Meaning |
|---|---|
<T extends U> | T must be assignable to U |
<T extends object> | T is an object type |
<T extends { id: string }> | T has an id |
<T extends string | number> | T is string or number |
<T extends HasId> | T has HasId’s shape |
<K extends keyof T> | K is a key of T |
<T extends new () => U> | T is a constructor |
<T extends A & B> | T satisfies both |
Common Constraints
| Constraint | Effect |
|---|---|
object | Not a primitive |
{ id: string } | Has an id |
keyof T | Valid key of T |
new () => U | Newable |
(...args: any[]) => U | Callable |
string | number | Union of primitives |
HasId & Serializable | Intersection |
keyof Patterns
| Pattern | Signature |
|---|---|
| Get | <T, K extends keyof T>(o: T, k: K): T[K] |
| Set | <T, K extends keyof T>(o: T, k: K, v: T[K]): void |
| Pluck | <T, K extends keyof T>(xs: T[], k: K): T[K][] |
| Pick | <T, K extends keyof T>(o: T, ks: K[]): Pick<T, K> |
| Group by | <T, K extends keyof T>(xs: T[], k: K): Record<T[K], T[]> |
Constructor Constraints
| Pattern | Signature |
|---|---|
| Factory | <T extends new () => U>(C: T): InstanceType<T> |
| With args | <T extends new (name: string) => object> |
| Registry | <T extends new () => object>(map: Map<string, T>, C: T) |
Constraint with Default
| Form | Meaning |
|---|---|
<T extends object = object> | Default to constraint |
<E extends Error = Error> | Default to Error |
<T extends Options = Options> | Default to shape |
<T = string> | No constraint, with default |
Multiple Constraints
| Form | Meaning |
|---|---|
<T extends A & B> | Both shapes |
<T extends object, U extends object> | Both objects |
<T, K extends keyof T> | K references T |
<T, K extends keyof T, V extends T[K]> | Value matches property type |
Where Constraints Apply
| Location | Example |
|---|---|
| Function | <T extends U>(x: T) |
| Arrow | <T extends U>(x: T) => x |
| Method | m<T extends U>(x: T) |
| Class | class C<T extends U> |
| Interface | interface I<T extends U> |
| Type alias | type T<X extends U> = ... |
Constraints vs Types
| Concept | Meaning |
|---|---|
| Constraint | Requirement on T |
| Intersection | Shape that has both |
| Union | Either type |
| InstanceType | Instance from constructor |
keyof | Keys of T |
Errors and Fixes
| Error | Cause | Fix |
|---|---|---|
Property does not exist on type T | No constraint | Add <T extends ...> |
not assignable to type | Constraint violated | Pass matching type |
Type argument not provided | Missing type | Specify <T> |
InstanceType requires constructor | Not newable | Constrain to constructor |
Cannot use T as value | Type vs value | Pass separately |
Common Utility Types
| Type | Meaning |
|---|---|
keyof T | Union of keys |
T[K] | Property type |
Pick<T, K> | Subset |
Omit<T, K> | T without K |
InstanceType<T> | Instance type |
Record<K, V> | Map from K to V |
Partial<T> | All optional |
Best Practices
✅ Do This:
// Constrain when accessing properties
function getLength<T extends { length: number }>(x: T): number {
return x.length;
} // ✅
// Use interface constraints for shape requirements
function findById<T extends HasId>(items: T[], id: string) {
return items.find(item => item.id === id);
} // ✅
// Use keyof for key-based operations
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
} // ✅
// Combine constraints with intersection
function touch<T extends Entity & Timestamped>(x: T): T { } // ✅
// Constrain to constructor types for factories
function create<T extends new () => object>(C: T): InstanceType<T> {
return new C();
} // ✅
// Provide defaults for optional type parameters
class Result<T, E extends Error = Error> { } // ✅
// Constrain multiple parameters to reference each other
function set<T, K extends keyof T, V extends T[K]>(
o: T, k: K, v: V
): void { o[k] = v; } // ✅
// Document why the constraint exists
// "T must have id so we can index by it" // ✅
❌ Don’t Do This:
// Don't access properties without a constraint
function f<T>(x: T) {
return x.length; // ❌ // ❌
}
// Don't over-constrain with unnecessary requirements
function log<T extends { id: string; name: string; ... }>(x: T) { }
// Too many requirements // ⚠️
// Don't use `any` to silence constraint errors
function f<T>(x: T) {
return (x as any).length; // ⚠️ defeats the purpose // ⚠️
}
// Don't constrain to a union that's too narrow
function f<T extends 'a' | 'b' | 'c'>(x: T) { }
// Overly restrictive // ⚠️
// Don't forget defaults when they'd help
function f<T extends object>() { }
// Caller must always specify T // ⚠️
// Don't use object when you need a specific shape
function f<T extends object>(x: T) {
return x.id; // ❌ object has no id // ❌
}
// Don't mix up `extends` meanings
// Constraint `T extends U` is NOT inheritance // ⚠️
// Don't forget T[K] in value position
function set<T, K extends keyof T>(o: T, k: K, v: T) {
o[k] = v; // ❌ v might not match T[K] // ❌
}
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Missing constraint | Can’t access property | Add <T extends ...> |
| Over-constrained | Too narrow | Loosen |
object when shape needed | No properties | Specify shape |
Wrong keyof order | Compile error | <T, K extends keyof T> |
T[K] vs T | Wrong type | Use indexed access |
| Constructor not newable | Error | <T extends new () => U> |
| Missing default | Caller must specify | Add default |
| Intersection too strict | Nothing matches | Use union or loose |
extends misread | Inheritance confusion | It’s assignability |
Real-World Examples
1. Length constraint
function getLength<T extends { length: number }>(x: T): number {
return x.length;
}
2. Shape constraint
function getId<T extends { id: string }>(x: T): string {
return x.id;
}
3. Interface constraint
interface HasId { id: string; }
function findBy<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(i => i.id === id);
}
4. keyof getter
function get<T, K extends keyof T>(o: T, k: K): T[K] {
return o[k];
}
5. keyof setter
function set<T, K extends keyof T>(o: T, k: K, v: T[K]): void {
o[k] = v;
}
6. Pluck
function pluck<T, K extends keyof T>(xs: T[], k: K): T[K][] {
return xs.map(x => x[k]);
}
7. Pick
function pick<T, K extends keyof T>(o: T, ks: K[]): Pick<T, K> {
return ks.reduce((acc, k) => ({ ...acc, [k]: o[k] }), {} as Pick<T, K>);
}
8. Multiple constraints
function touch<T extends Entity & Timestamped>(x: T): T {
x.updatedAt = new Date();
return x;
}
9. Union constraint
function stringify<T extends string | number>(x: T): string {
return String(x);
}
10. Object constraint
function keys<T extends object>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[];
}
11. Constructor constraint
function create<T extends new () => object>(Ctor: T): InstanceType<T> {
return new Ctor();
}
12. Constructor with args
function createWith<T extends new (name: string) => object>(
Ctor: T, name: string
): InstanceType<T> {
return new Ctor(name);
}
13. Constraint with default
class Result<T, E extends Error = Error> { }
14. Chained constraints
function setProperty<T, K extends keyof T, V extends T[K]>(
obj: T, key: K, value: V
): void {
obj[key] = value;
}
15. Array element constraint
function first<T extends { id: string }>(items: T[]): T | undefined {
return items[0];
}
16. Comparable constraint
function max<T extends { value: number }>(items: T[]): T | undefined {
return items.reduce((a, b) => !a || b.value > a.value ? b : a, undefined as T | undefined);
}
17. Function constraint
function wrap<T extends (...args: any[]) => any>(fn: T): T {
return fn;
}
18. Key from another type
function groupBy<T, K extends keyof T>(items: T[], key: K): Map<T[K], T[]> {
const map = new Map<T[K], T[]>();
for (const item of items) {
const k = item[key];
const list = map.get(k) ?? [];
list.push(item);
map.set(k, list);
}
return map;
}
19. Serializable constraint
interface Serializable { serialize(): string; }
function save<T extends Serializable>(item: T): void {
console.log(item.serialize());
}
20. Entity with index constraint
function indexBy<T extends { id: string }>(items: T[]): Map<string, T> {
return new Map(items.map(i => [i.id, i]));
}
Visual: Constraint Flow
┌──────────────────────────────────────────────┐
│ function f<T extends { id: string }>(x: T) {│
│ return x.id; │
│ } │
│ │
└──────────────────────────────────────────────┘
│
│ called with
▼
┌──────────────────────────────────────────────┐
│ f({ id: '1', name: 'Alice' }) │
│ │ │
│ ▼ │
│ T = { id: string; name: string } │
│ Constraint: ✅ has id │
│ Body: x.id works │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ f({ name: 'Alice' }) │
│ │ │
│ ▼ │
│ T = { name: string } │
│ Constraint: ❌ missing id │
│ Compile error │
│ │
└──────────────────────────────────────────────┘
Visual: keyof Constraint
┌──────────────────────────────────────────────┐
│ function get<T, K extends keyof T>( │
│ obj: T, key: K │
│ ): T[K] { │
│ return obj[key]; │
│ } │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ const user = { id: 1, name: 'Alice' }; │
│ │
│ get(user, 'id') → K = 'id' │
│ → T[K] = number │
│ │
│ get(user, 'name') → K = 'name' │
│ → T[K] = string │
│ │
│ get(user, 'x') → ❌ not keyof T │
│ │
└──────────────────────────────────────────────┘
Visual: Multiple Constraints
┌──────────────────────────────────────────────┐
│ function touch< │
│ T extends Entity & Timestamped │
│ >(x: T): T { │
│ x.updatedAt = new Date(); │
│ return x; │
│ } │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ T must have: │
│ │
│ From Entity: id: string │
│ From Timestamped: createdAt, updatedAt │
│ │
│ Missing either → compile error │
│ │
└──────────────────────────────────────────────┘
Visual: Constructor Constraint
┌──────────────────────────────────────────────┐
│ function create< │
│ T extends new () => object │
│ >(Ctor: T): InstanceType<T> { │
│ return new Ctor(); │
│ } │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ class User { name = 'Alice' } │
│ │
│ const u = create(User); │
│ │ │
│ ▼ │
│ T = typeof User │
│ InstanceType<T> = User │
│ │
└──────────────────────────────────────────────┘
Visual: Constraint vs Union
┌──────────────────────────────────────────────┐
│ Constraint │
│ │
│ <T extends { id: string }> │
│ │
│ T can be any type with id: string │
│ T is inferred from arguments │
│ Caller passes a value of some type │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Union type │
│ │
│ function f(x: { id: string } | User) │
│ │
│ x is exactly one of the listed types │
│ No generic parameter │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Constraint preserves T's exact type │
│ Union fixes the type to the listed ones │
│ │
└──────────────────────────────────────────────┘
Visual: Constraint Resolution
┌──────────────────────────────────────────────┐
│ function getLength<T extends { length: number }>(x: T)│
│ │
└──────────────────────────────────────────────┘
│
│ called
▼
┌──────────────────────────────────────────────┐
│ getLength('hello') │
│ │ │
│ ▼ │
│ T = string │
│ Check: string extends { length: number }? │
│ ✅ yes │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ getLength(42) │
│ │ │
│ ▼ │
│ T = number │
│ Check: number extends { length: number }? │
│ ❌ no │
│ Compile error │
│ │
└──────────────────────────────────────────────┘
Visual: Default + Constraint
┌──────────────────────────────────────────────┐
│ class Result<T, E extends Error = Error> { │
│ constructor( │
│ public ok: boolean, │
│ public value?: T, │
│ public error?: E │
│ ) {} │
│ } │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ new Result<number>(true, 42) │
│ │ │
│ ▼ │
│ E uses default Error │
│ Constraint satisfied │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ new Result<number, TypeError>(...) │
│ │ │
│ ▼ │
│ E = TypeError │
│ TypeError extends Error ✅ │
│ │
└──────────────────────────────────────────────┘
Visual: Chained Constraints
┌──────────────────────────────────────────────┐
│ function set< │
│ T, │
│ K extends keyof T, │
│ V extends T[K] │
│ >(obj: T, key: K, value: V): void { │
│ obj[key] = value; │
│ } │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ const user = { id: 1, name: 'Alice' }; │
│ │
│ set(user, 'name', 'Alice') ✅ │
│ set(user, 'name', 42) ❌ │
│ set(user, 'x', 'y') ❌ │
│ │
│ K restricted to keys of T │
│ V restricted to T[K] │
│ │
└──────────────────────────────────────────────┘
Visual: Decision Flow
┌──────────────────────────────────────────────┐
│ Does T need a property? │
│ │ │
│ ├── Yes ──► <T extends { prop: U }> │
│ │ │
│ └── No ──► Plain generic │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Is one parameter related to another? │
│ │ │
│ ├── Yes ──► <T, K extends keyof T> │
│ │ │
│ └── No ──► Independent parameters │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Need a constructor? │
│ │ │
│ └── Yes ──► <T extends new () => U> │
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | Meaning |
|---|---|
| Constraint | T extends U — requirement |
| Shape constraint | { id: string } |
| Interface constraint | HasId |
keyof constraint | K extends keyof T |
| Constructor constraint | T extends new () => U |
| Union constraint | string | number |
| Intersection constraint | A & B |
| Default with constraint | <T extends U = U> |
InstanceType<T> | Instance from constructor type |
Key takeaways:
- A constraint —
T extends U— requiresTto be assignable toU - Constraints let the function access properties of
Tthat would otherwise be unavailable extendsin a constraint means “assignable to,” not inheritance- Shape constraints —
T extends { id: string }— require specific properties - Interface constraints —
T extends HasId— require a named shape keyofconstraints —K extends keyof T— restrict a type parameter to keys of another- Constructor constraints —
T extends new () => U— require a newable thing - Union constraints —
T extends string \| number— restrict to specific types - Intersection constraints —
T extends A & B— require both shapes - Chained constraints —
<T, K extends keyof T, V extends T[K]>— link parameters together - Defaults —
<E extends Error = Error>— apply when the type is omitted InstanceType<T>extracts the instance type from a constructor type- Constraints are checked at every call site — mismatches fail to compile
Remember: Constraints turn a generic from “works with anything” into “works with anything that has what I need.” Every non-trivial generic function uses at least one. T extends { id: string } for identity operations, K extends keyof T for property access, T extends new () => U for factories — these are the three most common patterns. Constraints are what let the compiler check your requirements while still giving you the flexibility of generics. They’re the discipline that makes generics useful.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!