Generic Constraints & Defaults

Reviewed & published by Brayan K

A generic constraint, written <T extends Constraint>, restricts a type parameter to types that have a required shape so the function body can use those members safely, while a default type parameter, <T = SomeType>, supplies a fallback type when the caller doesn't provide or imply one.

Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

An unconstrained generic is a job posting that says "anyone may apply". You can't ask applicants to do anything specific because you don't know what they can do. A constraint is a job requirement — "must have a driving licence" (extends { length: number }). Now you can confidently ask every applicant to drive (read .length), and anyone without the licence is rejected at the door (a compile error). A default type is the "if unspecified, we'll assume X" line on the form — a sensible fallback so the common case needs no extra paperwork.

1. <T extends Constraint>

A bare <T> could be anything, so you can't touch any of its properties. Adding extends a shape promises those members exist. The function stays generic — it accepts strings, arrays, or any object with a length — yet the body can safely read .length:

// Constrain T to anything with a numeric length
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

longest("hello", "hi");      // ✅ strings have length -> string
longest([1, 2, 3], [9]);     // ✅ arrays have length -> number[]
longest(10, 20);             // ❌ number has no 'length' property

// Returning T (not the constraint) preserves the caller's exact type
function logLength<T extends { length: number }>(item: T): T {
  console.log(item.length);
  return item;                 // caller still gets the precise type back
}
// === Generic constraints: <T extends Constraint> ===
// A constraint promises that T has certain members, so the function body
// can safely use them. In TS:
//   function longest<T extends { length: number }>(a: T, b: T): T { ... }
// The constraint "{ length: number }" guarantees .length exists.
function longest(a, b) {
  return a.length >= b.length ? a : b;   // safe: both have .length
}
console.log(longest("hello", "hi"));            // hello
console.log(longest([1, 2, 3], [9]));           // [ 1, 2, 3 ]
console.log(longest("ab", "abcd"));             // abcd

// Without the constraint, .length might not exist (e.g. on a number),
// and TypeScript would refuse to compile. The constraint is what makes
// the property access provably safe.

// Constraints also let us return a precise type. Here logLength returns
// the same value it was given, so the caller keeps the exact type:
function logLength(item) {
  console.log("length is", item.length);
  return item;
}
const back = logLength([10, 20]);   // length is 2
console.log("got back:", back);     // got back: [ 10, 20 ]

2. Default Parameters & Multiple Constraints

A default type parameter <T = string> is used when the caller neither specifies T explicitly nor gives an argument TypeScript can infer it from. You can also declare several type parameters, each with its own constraint, and combine constraints with defaults:

// Default type parameter — falls back to string
function makeBox<T = string>(value: T): { value: T } {
  return { value };
}
makeBox();          // T defaults; { value: string }
makeBox(42);        // inferred: { value: number }

// Multiple constrained parameters — both must be objects
function merge<A extends object, B extends object>(a: A, b: B): A & B {
  return { ...a, ...b };
}
const m = merge({ name: "Alice" }, { age: 30 });
// type of m is { name: string } & { age: number }

// Constraint AND default together
function wrap<T extends object = {}>(x: T): { data: T } {
  return { data: x };
}
// === Default type parameters: <T = string> ===
// A generic can have a DEFAULT type, used when the caller doesn't specify
// (and TypeScript can't infer) one. In TS:
//   function makeBox<T = string>(value: T): { value: T } { ... }
// Runtime analogue: a normal default value when no argument is passed.
function makeBox(value = "default") {     // mirrors <T = string>
  return { value };
}
console.log(makeBox());          // { value: 'default' }  (used the default)
console.log(makeBox(42));        // { value: 42 }
console.log(makeBox(true));      // { value: true }

// Multiple constrained params — a "merge" that keeps BOTH shapes.
// In TS:  function merge<A extends object, B extends object>(a: A, b: B): A & B
function merge(a, b) {
  return { ...a, ...b };          // result has all keys of both
}
const merged = merge({ name: "Alice" }, { age: 30 });
console.log("merged:", merged);   // merged: { name: 'Alice', age: 30 }
console.log("keys:", Object.keys(merged)); // keys: [ 'name', 'age' ]

3. Constraining to keyof — a Safe Getter

The most useful constraint pairs two parameters: <T, K extends keyof T>. The second parameter is forced to be a real key of the first, so a typo becomes a compile error and the return type T[K] is exactly the type of that property:

// K must be one of T's real keys; return type is the property's type
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, name: "Alice", admin: true };
getProp(user, "name");   // ✅ typed as string  -> "Alice"
getProp(user, "id");     // ✅ typed as number   -> 1
getProp(user, "nope");   // ❌ '"nope"' is not assignable to keyof user

// A matching setter — the value must fit the property's type
function setProp<T, K extends keyof T>(obj: T, key: K, value: T[K]): T {
  obj[key] = value;
  return obj;
}
// === Constraining to keyof — a type-safe property getter ===
// The classic example: getProp(obj, key) returns obj[key], and the constraint
//   <T, K extends keyof T>  guarantees key is a REAL key of obj.
// At runtime, that's a safe indexed read:
function getProp(obj, key) {
  return obj[key];
}
const user = { id: 1, name: "Alice", admin: true };
console.log(getProp(user, "name"));   // Alice
console.log(getProp(user, "id"));     // 1
console.log(getProp(user, "admin"));  // true

// In TypeScript, getProp(user, "nope") would FAIL to compile because
// "nope" is not assignable to keyof typeof user. The constraint K extends
// keyof T turns a typo into a compile error instead of a runtime undefined.

// A setter version, same idea — key must be a real key:
function setProp(obj, key, value) {
  obj[key] = value;
  return obj;
}
const updated = setProp({ ...user }, "name", "Bob");
console.log("after set:", updated.name);   // after set: Bob

🎯 Your Turn

Implement the body of a function whose generic is constrained to anything with a length. Fill in the blanks and match the expected output.

// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// Mirror a constrained generic:
//   function firstItem<T extends { length: number }>(list: T) { ... }
// The constraint guarantees list has .length and is indexable.

function firstItem(list) {
  // 1) If the list has at least one item, return the first one...
  return list.length ___ 0 ? ___ : "empty";  // 👉 > 0  and  list[0]
}

console.log(firstItem([10, 20, 30]));  // 10
console.log(firstItem("hello"));       // h   (strings have length too!)
console.log(firstItem([]));            // empty

// ✅ Expected output:
//    10
//    h
//    empty

Common Errors (and the fix)

Pro Tips

Mini-Challenge: pluck

Build a type-safe pluck that extracts one property from every object in an array — the runtime form of <T, K extends keyof T>. Follow the outline, run it, and match the example output.

// 🎯 MINI-CHALLENGE: a type-safe "pluck"
// In TS this constrains the key to be a real property of the items:
//   function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] { ... }
//
// Write pluck(items, key) that:
//   1. Takes an array of objects and a key name.
//   2. Returns a new array of just that property from each object.
//      (Hint: items.map((item) => item[key]))
//
// Test data:
//   const people = [
//     { name: "Alice", age: 30 },
//     { name: "Bob",   age: 25 },
//     { name: "Carol", age: 41 },
//   ];
//   pluck(people, "name")  and  pluck(people, "age")
//
// ✅ Example output:
//    [ 'Alice', 'Bob', 'Carol' ]
//    [ 30, 25, 41 ]

// your code here

🎉 Lesson Complete

Practice quiz

What does '<T extends { length: number }>' promise inside the function body?

  • T is always a string
  • T is an array
  • T has a numeric 'length' property you can safely use
  • T is unknown

Answer: T has a numeric 'length' property you can safely use. The constraint guarantees the member exists, so reading a.length is provably safe.

Why can't you read a property off an unconstrained '<T>'?

  • T could be anything (e.g. a number), so no property is provably present
  • Generics are slow
  • TypeScript forbids all generics
  • It needs a default

Answer: T could be anything (e.g. a number), so no property is provably present. Without a constraint, the compiler knows nothing about T, so member access is rejected.

What is the difference between a constraint and a default type parameter?

  • They are the same
  • A default limits T; a constraint sets a fallback
  • Both supply fallbacks
  • A constraint LIMITS what T can be; a default supplies a FALLBACK when T is not given/inferred

Answer: A constraint LIMITS what T can be; a default supplies a FALLBACK when T is not given/inferred. <T extends U> restricts T; <T = U> provides a fallback only when T is not specified or inferred.

What does 'keyof T' produce?

  • The values of T
  • The union of all of T's property names
  • A new object
  • T[K]

Answer: The union of all of T's property names. keyof T is the union of T's property name literals.

In 'function getProp<T, K extends keyof T>(obj: T, key: K)', what is the return type?

  • T[K] (the property's type)
  • any
  • T
  • K

Answer: T[K] (the property's type). The lookup type T[K] is exactly the type of the selected property.

What does the K extends keyof T constraint catch at compile time?

  • Slow code
  • Missing return statements
  • A bad/typo key like getProp(user, "naem")
  • Circular types

Answer: A bad/typo key like getProp(user, "naem"). Passing a key that is not a real property of T becomes a compile error, not a runtime undefined.

When does a default type parameter '<T = string>' actually apply?

  • Always
  • Only when T is neither specified explicitly nor inferable from an argument
  • Only with keyof
  • Never

Answer: Only when T is neither specified explicitly nor inferable from an argument. If TypeScript can infer T from an argument, the inferred type wins over the default.

What is the return type of 'merge<A extends object, B extends object>(a: A, b: B)'?

  • object
  • A | B
  • unknown
  • A & B

Answer: A & B. The intersection A & B has all properties of both inputs, keeping the merged object fully typed.

Why declare 'T' before 'K' in '<T, K extends keyof T>'?

  • Alphabetical order
  • A parameter can only be constrained by ones declared before it
  • K is faster
  • It is optional

Answer: A parameter can only be constrained by ones declared before it. <K extends keyof T, T> errors because K references T before it is declared.

How do you both restrict a type parameter and give it a fallback?

  • <T = object extends {}>
  • <T: object | {}>
  • <T extends object = {}>
  • You cannot combine them

Answer: <T extends object = {}>. <T extends object = {}> means T must be an object and defaults to {} when not provided.

Continue this course

Frequently asked questions

Why constrain a generic instead of just using a plain type parameter?

An unconstrained <T> is a black box — TypeScript knows nothing about it, so you can't access any property safely. The moment you write a.length inside the function, the compiler complains because a generic T might be a number. Adding <T extends { length: number }> tells the compiler 'whatever T turns out to be, it definitely has a numeric length', which both unlocks the property access inside the body and rejects callers who pass something without it. Constraints are how you keep a function generic while still using the parts of the value you need.

What's the difference between a constraint and a default type parameter?

A constraint (<T extends U>) LIMITS what T can be — callers must pass a type assignable to U. A default (<T = U>) supplies a FALLBACK type used only when T isn't given and can't be inferred. They are independent and often combined: <T extends object = {}> means 'T must be an object, and if you don't specify one, assume the empty object'. Think of constraint as the rule and default as the no-argument behaviour.

How does <K extends keyof T> make a property getter type-safe?

keyof T is the union of all of T's property names. Constraining K to extend keyof T forces the key argument to be one of those real names, so getProp(user, 'naem') is a compile error rather than a silent undefined at runtime. The return type T[K] (a lookup type) is then exactly the type of that property — so getProp(user, 'age') is typed as number, not any. This pattern is the foundation of every type-safe accessor, form helper, and ORM column selector.

Can one type parameter's constraint reference another?

Yes, and it's extremely common. In function getProp<T, K extends keyof T>, the constraint on K depends on T. TypeScript resolves them together: it first works out T from the object argument, then checks K against that specific T's keys. You can chain further — for example <T, K extends keyof T, V extends T[K]> — to constrain a value to match the type of the selected property. Order matters: a parameter can only be constrained by ones declared before it.