Skip to content
Laviyx Learn

Try

TypeScript course menu

TypeScript Generics: Reusable, Type-Safe Functions, Types and Classes

Learn TypeScript generics from scratch: why any loses information, generic functions with type parameters, type argument inference, generic types and interfaces, generic classes, multiple type parameters, and the generics you already use such as Array<T> and Promise<T>.

Advanced8 min readUpdated
On this page

Generics let you write code that works with many types while still keeping track of which type is being used. They are how Array<string>, Promise<User> and Map<string, number> work, and they are essential for writing reusable functions and data structures.

The problem#

Here is a function that returns the first item of an array. How should it be typed?

TypeScript
function firstAny(items: any[]): any {
  return items[0];
}

const n = firstAny([10, 20]);   // type: any
console.log(n.toUpperCase?.());  // no error from TypeScript, even though n is a number
Output
undefined

With any, the function accepts everything but loses the type: the caller gets any back, and all checking stops. Writing a separate function for each type (firstNumber, firstString) would be absurd.

A generic function#

A type parameter, written in angle brackets, acts like a variable for a type:

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

const n = first([10, 20]);          // T is number, so n: number | undefined
const s = first(["a", "b"]);        // T is string, so s: string | undefined

console.log((n ?? 0) + 1, s?.toUpperCase());
Output
11 A
  • <T> declares a type parameter called T (any name works; T for "type" is conventional).
  • items: T[] and the return type T | undefined both refer to it.
  • When you call first([10, 20]), TypeScript infers T = number from the argument, so the result is typed correctly.

The type is preserved, and mistakes are still caught:

TypeScript
function first<T>(items: T[]): T | undefined {
  return items[0];
}
const n = first([10, 20]);
n?.toUpperCase();
Compiler error
error TS2339: Property 'toUpperCase' does not exist on type 'number'.

You can also pass the type explicitly: first<string>(["a"]). Usually inference is enough.

More generic functions#

TypeScript
function last<T>(items: readonly T[]): T | undefined {
  return items.at(-1);
}

function pair<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}

function mapValues<T, R>(items: T[], fn: (item: T) => R): R[] {
  return items.map(fn);
}

const lengths = mapValues(["html", "css"], (word) => word.length);   // number[]
console.log(last([1, 2, 3]), pair("id", 42), lengths);
Output
3 [ 'id', 42 ] [ 4, 3 ]

Multiple type parameters (A, B, or T and R for "result") let you relate several types. In mapValues, the callback's parameter word is inferred as string and its return type number becomes R.

Generic types and interfaces#

Types can have type parameters too:

TypeScript
interface ApiResponse<T> {
  ok: boolean;
  data: T;
  fetchedAt: string;
}

type Pair<K, V> = { key: K; value: V };

const userResponse: ApiResponse<{ id: number; name: string }> = {
  ok: true,
  data: { id: 1, name: "Asha" },
  fetchedAt: "2026-10-24",
};
const setting: Pair<string, boolean> = { key: "darkMode", value: true };

console.log(userResponse.data.name, setting.value);
Output
Asha true

Generic classes#

TypeScript
class Stack<T> {
  #items: T[] = [];

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

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

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

const history = new Stack<string>();
history.push("/home");
history.push("/courses");
console.log(history.pop(), history.size);
Output
/courses 1
TypeScript
class Stack<T> {
  #items: T[] = [];
  push(item: T): void {
    this.#items.push(item);
  }
}
const history = new Stack<string>();
history.push(404);
Compiler error
error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.

Generics you already use#

Generic typeMeaning
Array<T> (or T[])An array of T
Promise<T>A promise that resolves to a T
Map<K, V>, Set<T>Keyed collections
Record<K, V>An object with keys K and values V
ReadonlyArray<T>An array that cannot be changed
TypeScript
const scores = new Map<string, number>([["asha", 92]]);
const loaded: Promise<string[]> = Promise.resolve(["HTML", "CSS"]);

console.log(scores.get("asha"), (await loaded).length);
Output
92 2

When to use generics#

Use a type parameter when it connects two or more places: an argument and the return value, two arguments, or a property and a method. If a type parameter appears only once, you probably do not need it; a plain type (or unknown) is simpler.

Practice#

Practice exercise

Write a generic function groupBy<T, K extends string>(items: T[], getKey: (item: T) => K): Record<K, T[]> (the extends string part is explained in the next lesson), and use it to group students by grade.

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

const students = [
  { name: "Asha", grade: "A" },
  { name: "Ravi", grade: "B" },
  { name: "Meera", grade: "A" },
];

const byGrade = groupBy(students, (s) => s.grade);
console.log(byGrade.A.map((s) => s.name), byGrade.B.length);
Output
[ 'Asha', 'Meera' ] 1

{} as Record<K, T[]> starts with an empty object asserted to the final type, because it is filled in by the loop.

Key takeaway#

Finished this lesson?