| |

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 T to 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 extends and not something else: extends in 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 HasId says “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 T is 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 InstanceType exists: A constructor type and an instance type are different. typeof User is the constructor; User is 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 & B uses 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 T isn’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 = Error follows 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:

  • Entity constraint — findById, indexBy
  • keyof constraint — pluck
  • Multiple constraints — touch requires Entity & 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. findById needs an ID; pluck needs a valid key; touch needs both an entity and a timestamp; create needs 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

FormMeaning
<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

ConstraintEffect
objectNot a primitive
{ id: string }Has an id
keyof TValid key of T
new () => UNewable
(...args: any[]) => UCallable
string | numberUnion of primitives
HasId & SerializableIntersection

keyof Patterns

PatternSignature
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

PatternSignature
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

FormMeaning
<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

FormMeaning
<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

LocationExample
Function<T extends U>(x: T)
Arrow<T extends U>(x: T) => x
Methodm<T extends U>(x: T)
Classclass C<T extends U>
Interfaceinterface I<T extends U>
Type aliastype T<X extends U> = ...

Constraints vs Types

ConceptMeaning
ConstraintRequirement on T
IntersectionShape that has both
UnionEither type
InstanceTypeInstance from constructor
keyofKeys of T

Errors and Fixes

ErrorCauseFix
Property does not exist on type TNo constraintAdd <T extends ...>
not assignable to typeConstraint violatedPass matching type
Type argument not providedMissing typeSpecify <T>
InstanceType requires constructorNot newableConstrain to constructor
Cannot use T as valueType vs valuePass separately

Common Utility Types

TypeMeaning
keyof TUnion 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

PitfallProblemSolution
Missing constraintCan’t access propertyAdd <T extends ...>
Over-constrainedToo narrowLoosen
object when shape neededNo propertiesSpecify shape
Wrong keyof orderCompile error<T, K extends keyof T>
T[K] vs TWrong typeUse indexed access
Constructor not newableError<T extends new () => U>
Missing defaultCaller must specifyAdd default
Intersection too strictNothing matchesUse union or loose
extends misreadInheritance confusionIt’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

ConceptMeaning
ConstraintT extends U — requirement
Shape constraint{ id: string }
Interface constraintHasId
keyof constraintK extends keyof T
Constructor constraintT extends new () => U
Union constraintstring | number
Intersection constraintA & B
Default with constraint<T extends U = U>
InstanceType<T>Instance from constructor type

Key takeaways:

  • A constraintT extends U — requires T to be assignable to U
  • Constraints let the function access properties of T that would otherwise be unavailable
  • extends in a constraint means “assignable to,” not inheritance
  • Shape constraintsT extends { id: string } — require specific properties
  • Interface constraintsT extends HasId — require a named shape
  • keyof constraintsK extends keyof T — restrict a type parameter to keys of another
  • Constructor constraintsT extends new () => U — require a newable thing
  • Union constraintsT extends string \| number — restrict to specific types
  • Intersection constraintsT 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!