TypeScript Fundamentals: Types, Generics and Real-World Patterns · Generics, Utility Types and Real-World TypeScript

Generics: Reusable, Type-Safe Code

Write functions, interfaces and classes that work with many types without losing type information, constrain type parameters with extends and keyof, and use defaults.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A generic Stack<T>, a pluck function that reads a property from a list of objects with full type safety, and a Result<T, E> type with defaults.

The idea

A generic is a type parameter: a placeholder for a type chosen by the caller. It lets one definition work for many types while keeping the relationship between input and output.

function first<T>(items: T[]): T | undefined { return items[0]; }

first([1, 2]) returns number | undefined; first(["a"]) returns string | undefined. Without generics you would use any and lose that information.

Useful tools:

  • Inference: you rarely write first<number>(...); TypeScript infers T from the arguments.
  • Constraints: T extends { id: number } requires that T has an id. Inside the function you can use .id safely.
  • keyof T: a union of the property names of T. K extends keyof T ties a key to its object, so obj[key] has the exact right type.
  • Defaults: interface Result<T, E = Error> lets callers omit the second type.
  • Generic classes and interfaces: class Stack<T>, interface Repository<T>.

Generics are about relationships. If a type parameter appears only once in a signature, you probably don't need it.

The code

Generic class, constraints and keyofTry it yourself
class Stack<T> {
  private items: T[] = [];

  push(item: T): this {
    this.items.push(item);
    return this;
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  get size(): number {
    return this.items.length;
  }
}

const numbers = new Stack<number>().push(1).push(2).push(3);
console.log(numbers.pop(), numbers.size);

// K extends keyof T ties the key to the object
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
  return items.map((item) => item[key]);
}

const people = [
  { id: 1, name: "Ada", age: 36 },
  { id: 2, name: "Grace", age: 45 },
];

const names = pluck(people, "name");   // string[]
const ages = pluck(people, "age");     // number[]
console.log(names, ages);

// @ts-expect-error: "email" is not a key of the person type
pluck(people, "email");

// A constraint: T must have an id
function indexById<T extends { id: number }>(items: T[]): Map<number, T> {
  return new Map(items.map((item) => [item.id, item]));
}
console.log(indexById(people).get(2)?.name);

// A type parameter with a default
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };

function safeParse(text: string): Result<number> {
  const value = Number(text);
  return Number.isNaN(value) ? { ok: false, error: new Error(`not a number: ${text}`) } : { ok: true, value };
}

for (const input of ["42", "abc"]) {
  const result = safeParse(input);
  console.log(result.ok ? `value ${result.value}` : result.error.message);
}

Output

3 2
[ 'Ada', 'Grace' ] [ 36, 45 ]
Grace
value 42
not a number: abc

Common mistakes

  • Adding a type parameter that is used once. function log<T>(x: T): void gains nothing over (x: unknown).
  • Forgetting constraints and then reaching for any. If you need .id or .length, constrain with extends.
  • Passing type arguments you don't need. Let inference work; add <...> only when it cannot infer (for example new Stack<number>()).
  • Over-engineering with deep generic chains. If readers can't tell what a signature means, simplify it.
  • Confusing T the type with a runtime value. Type parameters are erased; you cannot do new T() or typeof T.

Key points

  • Generics are type parameters that preserve relationships between inputs and outputs.
  • Constrain with extends; tie keys to objects with K extends keyof T.
  • Defaults (E = Error) make generic types easier to use.
  • Prefer inference; only supply type arguments when the compiler cannot work them out.

Further reading: The Handbook: Generics.

Practice questions

What does `K extends keyof T` express in `function pluck<T, K extends keyof T>(items: T[], key: K): T[K][]`?

Answer

That key must be one of the property names of T, and the return type is the array of that property's type, T[K][]. The key and the result type stay linked, so the compiler rejects unknown keys and knows the exact element type.

When is a generic type parameter unnecessary?

Answer

When it appears only once in the signature and doesn't relate two types. For example function log<T>(x: T) could just take unknown, because nothing connects T to another part of the signature.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?