Problem Solved

TypeScript · generic constraints

Home › Courses › TypeScript › Problems › Constrain generic
🔷 Problem · 5 min read

How to constrain a generic type.
Tell the compiler what T can do.

You write function getLength(x: T) { return x.length; } and get an error: Property 'length' does not exist on type 'T'. Of course it doesn't — T could be a number. The fix is a constraint: tell the compiler that T has at least a length property. This is the pattern behind every typed helper you'll write — and behind the keyof pattern that powers every typed getter.

Short answer: Add extends to the type parameter: . The constraint tells the compiler "T has at least these properties," which unlocks accessing them inside the function. The pattern for typed property access: . Constraints can be object shapes, unions, or other type parameters.

✅ Solution 1 — Constrain to an object shape

When you need to access a property on T.

The most common constraint: "T must have this property." Without it, accessing x.length, x.id, or any specific property on a bare T fails. The constraint describes the minimum requirement — T can be anything that satisfies it, and you get access to the required fields.

constraints.ts
// ─── The problem: unconstrained T ────────────
function badLength(x: T): number {
// return x.length;
// ❌ Property 'length' does not exist on type 'T'.
// T could be a number, an object, anything — no guarantee
// it has 'length'.
return 0;
}

// ─── The fix: constrain T ────────────────────
function getLengthextends { length: number }>(x: T): number {
return x.length; // ✅ T guaranteed to have length: number
}

getLength('hello'); // ✅ string has length
getLength([1, 2, 3]); // ✅ array has length
getLength({ length: 5 }); // ✅ any object with length
// getLength(42); // ❌ number doesn't have length
// getLength({ name: 'x' }); // ❌ no length property

// ─── Constrain to a named interface ──────────
interface HasId {
id: string;
}

function findByIdextends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}

interface User { id: string; name: string }
interface Post { id: string; title: string }

const users: User[] = [{ id: '1', name: 'Alice' }];
const posts: Post[] = [{ id: 'p1', title: 'Hello' }];

findById(users, '1'); // ✅ returns User | undefined
findById(posts, 'p1'); // ✅ returns Post | undefined

// ─── Multiple constraints via & ──────────────
interface HasName { name: string }

function greetByIdextends HasId & HasName>(item: T): string {
return ${item.id}: ${item.name};
}

console.log(getLength('hello'), findById(users, '1'));
function badLength(x) {
return 0;
}

function getLength(x) {
return x.length;
}

getLength('hello');
getLength([1, 2, 3]);
getLength({ length: 5 });

function findById(items, id) {
return items.find(item => item.id === id);
}

const users = [{ id: '1', name: 'Alice' }];
const posts = [{ id: 'p1', title: 'Hello' }];

findById(users, '1');
findById(posts, 'p1');

function greetById(item) {
return ${item.id}: ${item.name};
}

console.log(getLength('hello'), findById(users, '1'));

Constraint = minimum

means "T can be any type that has at least length: number." Extra properties are fine.

Unlocks property access

Inside the function, T is guaranteed to have the constraint's properties. x.length compiles because the constraint says so.

Preserves caller's type

The caller still gets back the exact type passed in. findById(users, '1') returns User | undefined, not HasId | undefined.

Multiple via &

— T must satisfy both shapes. requires both properties.

Constraints are promises, not restrictions on you. Beginners think "constraint" means "narrow T down." It doesn't — it means "promise the compiler that T has at least these properties, and I'll use them." The caller can still pass anything that satisfies the promise. findById works for User, Post, Comment, or any future type with an id: string. The constraint is the smallest contract that lets the function do its job.

✅ Solution 2 — keyof constraint (typed property access)

The pattern behind every typed getter.

If you want a function that takes an object and a key, returns the value at that key, and preserves the specific type — keyof is the constraint. K extends keyof T means "K must be one of T's keys," and the return type T[K] is the exact type of the value at that key. This is the most-used generic pattern in library code.

keyof-constraint.ts
interface User {
id: string;
name: string;
age: number;
email: string;
}

// ─── The typed getter ────────────────────────
function getextends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

const user: User = { id: '1', name: 'Alice', age: 28, email: 'a@x.com' };

const n = get(user, 'name'); // n: string — exact type
const a = get(user, 'age'); // a: number — exact type
const e = get(user, 'email'); // e: string
// get(user, 'foo'); // ❌ 'foo' not a key of User

// ─── Pluck — pull a field from an array ──────
function pluckextends keyof T>(items: T[], key: K): T[K][] {
return items.map(item => item[key]);
}

const users: User[] = [
{ id: '1', name: 'Alice', age: 28, email: 'a@x.com' },
{ id: '2', name: 'Bob', age: 34, email: 'b@x.com' }
];

const names = pluck(users, 'name'); // string[]
const ages = pluck(users, 'age'); // number[]
// pluck(users, 'foo'); // ❌

// ─── Set a property by key ───────────────────
function setPropextends keyof T>(obj: T, key: K, value: T[K]): T {
return { ...obj, [key]: value };
}

const updated = setProp(user, 'name', 'Alicia'); // ✅ value must be string
// setProp(user, 'name', 42); // ❌ must be string
// setProp(user, 'age', 'x'); // ❌ must be number

// ─── Omit by key at runtime ──────────────────
function omitKeyextends keyof T>(obj: T, key: K): Omit {
const { [key]: _, ...rest } = obj;
return rest as Omit;
}

const noEmail = omitKey(user, 'email');
// noEmail: Omit = { id, name, age }

console.log(n, a, names, ages);
function get(obj, key) {
return obj[key];
}

const user = { id: '1', name: 'Alice', age: 28, email: 'a@x.com' };

const n = get(user, 'name');
const a = get(user, 'age');
const e = get(user, 'email');

function pluck(items, key) {
return items.map(item => item[key]);
}

const users = [
{ id: '1', name: 'Alice', age: 28, email: 'a@x.com' },
{ id: '2', name: 'Bob', age: 34, email: 'b@x.com' }
];

const names = pluck(users, 'name');
const ages = pluck(users, 'age');

function setProp(obj, key, value) {
return { ...obj, [key]: value };
}

const updated = setProp(user, 'name', 'Alicia');

function omitKey(obj, key) {
const { [key]: _, ...rest } = obj;
return rest;
}

const noEmail = omitKey(user, 'email');

console.log(n, a, names, ages);

K extends keyof T

K must be one of T's keys. get(user, 'name') works; get(user, 'foo') fails. The constraint IS the check.

T[K] — indexed access

The return type is the exact type of the value at key K. get(user, 'age') returns number, not string | number.

Value parameter matches key

setProp(obj: T, key: K, value: T[K]) — value must match the key's type. 'name' requires a string.

The library pattern

This is how lodash's get, Immer's drafts, and every typed object utility work. Learn it once, recognize it everywhere.

✅ Solution 3 — Constrain to a union

When T must be one of a fixed set of primitive types.

Sometimes you don't want any object — you want one of a specific set of types. restricts T to strings or numbers. This is a constraint too, just a narrower one. It's how you write sorting functions, comparison helpers, and any generic that only works with a subset of types.

union-constraint.ts
// ─── Constrain to primitives ─────────────────
type Sortable = string | number;

function sortArrayextends Sortable>(arr: T[]): T[] {
return [...arr].sort();
}

sortArray([3, 1, 2]); // ✅ number[]
sortArray(['c', 'a', 'b']); // ✅ string[]
// sortArray([{ id: 1 }]); // ❌ object not Sortable

// ─── Constrain to a literal union ────────────
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

function requestextends HttpMethod>(url: string, method: M): void {
console.log(method, url);
}

request('/api/users', 'GET'); // ✅
request('/api/users', 'POST'); // ✅
// request('/api/users', 'PATCH'); // ❌ 'PATCH' not in HttpMethod

// ─── Constrain to "has callable method" ──────
function callItextends { (): void }>(fn: T): void {
fn(); // ✅ T is guaranteed callable
}

callIt(() => console.log('hi')); // ✅
// callIt('not a function'); // ❌

// ─── Constraint referencing another param ────
function hasKeyextends keyof T>(obj: T, key: K): boolean {
return key in obj;
}

// K's constraint depends on T — that's fine.
// TypeScript fills in T first, then constrains K.

const user = { id: '1', name: 'Alice' };
hasKey(user, 'name'); // ✅ K='name'
// hasKey(user, 'foo'); // ❌ 'foo' not a key

// ─── Constraints compose with utility types ──
function ensureIdextends { id?: string }>(obj: T): T & { id: string } {
return { ...obj, id: obj.id ?? crypto.randomUUID() };
}

const withId = ensureId({ name: 'x' });
// withId: { name: string; id: string }

console.log(sortArray([3, 1, 2]), withId);
function sortArray(arr) {
return [...arr].sort();
}

sortArray([3, 1, 2]);
sortArray(['c', 'a', 'b']);

function request(url, method) {
console.log(method, url);
}

request('/api/users', 'GET');
request('/api/users', 'POST');

function callIt(fn) {
fn();
}

callIt(() => console.log('hi'));

function hasKey(obj, key) {
return key in obj;
}

const user = { id: '1', name: 'Alice' };
hasKey(user, 'name');

function ensureId(obj) {
return { ...obj, id: obj.id ?? crypto.randomUUID() };
}

const withId = ensureId({ name: 'x' });

console.log(sortArray([3, 1, 2]), withId);

Union constraints

— T is one of those. Useful for primitives-only generics (sorting, comparison, hashing).

Literal union constraints

— M is one of the literals. The most precise constraint: catches typos at compile time.

Callable constraint

void> — T must be callable. Lets you call T() inside the function without knowing its exact signature.

Cross-parameter constraints

— K's constraint depends on T. TypeScript resolves T first, then constrains K accordingly. Very common.

⚠️ Common mistakes

The generic constraint bugs that cost real time.

1. Forgetting to constrain

function f(x: T) { return x.length; } fails — T might be a number. Fix: .

2. Using any instead of constraining

function f(x: any) { return x.length; } works but loses all type info. Fix: generic + constraint. Same length, full safety.

3. Over-constraining

when you only need id. Fix: — minimal constraint, maximum reuse.

4. Trying multiple extends

is a syntax error. Fix: — combine with intersection.

5. Forgetting keyof in cross-parameter constraints

— K must be a key of T. Without keyof, K is unconstrained and T[K] fails. Fix: use the pattern exactly.

6. Constraint too specific

when you only access id. Fix: constrain only what you use. Smaller constraint = more types allowed = more reuse.

7. Constraint that breaks inference

(x: T) — T is inferred but T[K] doesn't apply. Fix: for primitives, constraining is fine; for objects with keys, use keyof.

8. Not constraining when inference breaks

function f(x: T, y: T) — T is inferred from both; a string and number conflict. Fix: constrain to the union, or use two parameters .

❓ Frequently asked

How do I constrain a generic type in TypeScript?

Use extends after the type parameter: function getLength(x: T). This restricts T to types with a length property. Without the constraint, T is unconstrained and you can't access any specific property.

Why do I need to constrain generics?

An unconstrained T could be anything — including types that don't have the property you want to access. Constraining tells the compiler 'T has at least a length property', which unlocks the access. Constraints are how you promise what T can do.

What is a keyof constraint?

keyof T produces a union of T's keys. Using it as a constraint: function get(obj: T, key: K): T[K]. K can only be a key of T, and the return type T[K] is the specific value type for that key. This is the pattern behind typed getters, pick functions, and every safe property access helper.

Can a generic have multiple constraints?

Not with multiple extends clauses. But you can combine constraints with intersection: . T must have both id and name. You can also constrain to a union (T extends string | number) — T is then one of those primitives.

🔗 Related

← All TypeScript problems
Problem solved · TypeScript