Generic Constraints and Default Type Parameters

Introduction

Unconstrained generics accept any type—which is sometimes too broad. Constraints (T extends SomeType) limit type parameters to shapes that support the operations you need. Default type parameters supply fallbacks when callers omit type arguments. Together they make generic APIs precise and ergonomic.

Prerequisites

Constraints with extends

Require that T has at least the shape of a type:

typescript
interface HasLength {
  length: number;
}
 
function logLength<T extends HasLength>(value: T): void {
  console.log(value.length);
}
 
logLength("hello");
logLength([1, 2, 3]);
// logLength(42);  // Error: number has no 'length'

Code explanation:

  • T extends HasLength means “any type assignable to HasLength
  • Inside the function, value.length is legal

Constrain to a specific key

typescript
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
 
const user = { id: 1, name: "Ada" };
const name = getProperty(user, "name");
// const bad = getProperty(user, "age");  // Error

Code explanation:

  • K extends keyof T limits key to real property names of obj
  • Return type T[K] links return type to the key

Constraining to Constructors

typescript
interface TimestampedInstance {
  createdAt: Date;
}
 
function create<T extends new (...args: never[]) => TimestampedInstance>(
  Ctor: T
): InstanceType<T> {
  const instance = new Ctor();
  instance.createdAt = new Date();
  return instance;
}

Advanced pattern—shows that constraints are not limited to object shapes. Most apps use simpler extends on interfaces.

Multiple Constraints (Intersection)

typescript
interface Printable {
  print(): void;
}
 
interface Saveable {
  save(): void;
}
 
function runWorkflow<T extends Printable & Saveable>(doc: T): void {
  doc.print();
  doc.save();
}

T must satisfy both interfaces.

Default Type Parameters

Provide a default when the type argument is omitted:

Code explanation:

  • Page without type argument uses unknown for items
  • Page<User> specializes TItem

Defaults on functions

typescript
function createState<TState extends object = Record<string, never>>(
  initial: TState
): { state: TState; reset(): void } {
  let state = initial;
  return {
    state,
    reset() {
      state = initial;
    },
  };
}

Combining Constraints and Defaults

typescript
interface Entity {
  id: string;
}
 
function findById<T extends Entity = Entity>(
  items: T[],
  id: string
): T | undefined {
  return items.find((item) => item.id === id);
}

Callers get Entity by default or a narrower type when items is more specific.

keyof Constraints in Practice

typescript
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 subset = pick(user, ["name"]);

Pick is a built-in utility type (covered later)—here it shows why K extends keyof T matters.

When Not to Constrain

If a generic truly works for all types (identity function), leave it unconstrained:

typescript
function identity<T>(value: T): T {
  return value;
}

Over-constraining blocks valid uses.

Comparison Table

TechniqueSyntaxEffect
Unconstrained<T>Any type
Shape constraint<T extends Foo>Must be assignable to Foo
Key constraint<K extends keyof T>Keys of T only
Default<T = Foo>Foo when omitted

Practical Example: Repository with Entity Constraint

Code explanation:

  • T extends Entity guarantees every stored item has id
  • Repository<Article> narrows returned items to Article

Common Mistakes

MistakeSymptomFix
Constraint too narrowValid types rejectedLoosen interface or split overloads
Using extends for unions incorrectlySyntax errorConstraints use extends, not | on the parameter
Default incompatible with constraintCompile error on definitionDefault must satisfy extends bound
any instead of genericLost safetyIntroduce <T>

FAQ

extends in generics vs class extends?

Same keyword, different jobs: generic T extends U bounds a type parameter; class extends inherits from a base class.

Can I use extends string?

Yes. T extends string allows string and string literal types—useful for branded strings in advanced patterns.

What is extends never?

Rare edge case in conditional types—not typical application code.

Order of multiple type parameters?

Defaults often appear on the rightmost parameters: <T, U = string> so callers can omit U but still specify T.

Constraint vs union on parameter?

T extends A | B is invalid as a single extends bound for a union—use T extends A or a shared base interface.

Do defaults work with inference?

Yes. Inference can still pick T while U falls back to its default when not specified.