IntermediateTypeScript · Lesson 2 of 9

Generics

Write functions and types that work with any type while staying type-safe.

Generics are type parameters. function first<T>(items: T[]): T | undefined works for arrays of numbers, strings or students — and the return type follows whatever goes in, instead of collapsing to any.

Constraints limit what a type parameter may be: <T extends { id: number }> accepts any object that has a numeric id.

Generic types are common for wrappers such as API responses (ApiResult<T>), caches and repositories.

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

function groupBy<T, K extends string | number>(items: T[], key: (item: T) => K): Record<K, T[]> {
  const groups = {} as Record<K, T[]>;
  for (const item of items) {
    const k = key(item);
    (groups[k] ??= []).push(item);
  }
  return groups;
}

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

type ApiResult<T> = { ok: true; data: T } | { ok: false; error: string };

function wrap<T>(data: T): ApiResult<T> {
  return { ok: true, data };
}

const people = [
  { id: 1, name: "Amina", form: 4 },
  { id: 2, name: "Juma", form: 3 },
  { id: 3, name: "Neema", form: 4 },
];

const n = first([10, 20, 30]);   // number | undefined
console.log(n, findById(people, 2)?.name);
console.log(groupBy(people, (p) => p.form));

const result = wrap(people.length);
if (result.ok) console.log("Count:", result.data);

Key points

  • Use a generic when the output type depends on the input type.
  • extends constrains a type parameter to shapes you can rely on.
  • Don't add generics that are used only once — they add noise without safety.

Exercise

Write a generic Stack<T> (as a class or functions) with push, pop and peek. Then write uniqueBy<T>(items, key) that removes duplicates using a key function.

Show solution

Try the exercise yourself first — then compare your approach with this one.

A generic class Stack<T> stores items of one type and returns that same type from pop and peek. uniqueBy remembers the keys it has already seen in a Set, keeping only the first item for each key.

stack.tsTypeScript
class Stack<T> {
  private items: T[] = [];

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

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

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

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

function uniqueBy<T, K>(items: T[], key: (item: T) => K): T[] {
  const seen = new Set<K>();
  return items.filter((item) => {
    const k = key(item);
    if (seen.has(k)) return false;
    seen.add(k);
    return true;
  });
}

const history = new Stack<string>();
history.push("Home");
history.push("Lessons");
console.log(history.pop(), history.peek(), history.size);   // Lessons Home 1

const entries = [
  { id: 1, name: "Amina" },
  { id: 2, name: "Juma" },
  { id: 1, name: "Amina (duplicate)" },
];
console.log(uniqueBy(entries, (e) => e.id).map((e) => e.name));   // [ 'Amina', 'Juma' ]

Check your understanding

  1. What is the main benefit of function first<T>(items: T[]): T | undefined over using any?

  2. What does <T extends { id: number }> mean?

  3. Which is a correctly typed generic API result?

  4. When should you avoid adding a generic type parameter?

Ask AI