Skip to content

Lesson 7 of 1012 min

Generics

Write a function or type once and use it with many types, without giving up type safety. Generics are type parameters.

In this lesson

  • Explain what a type parameter is
  • Write generic functions and types
  • Constrain generics with extends and keyof

The problem generics solve#

Imagine a function that returns the first item of an array. Without generics you either write one per type or give up type information.

first-any.ts
function firstAny(items: unknown[]): unknown {  return items[0];}const value = firstAny(["a", "b"]);1

value is unknown: TypeScript forgot that the array held strings, so you cannot call string methods without checking first.

Type parameters#

A generic function declares a type parameter in angle brackets. TypeScript fills it in from the arguments.

first.ts
function first<T>(items: T[]): T | undefined {1  return items[0];}const word = first(["a", "b"]);2const num = first([10, 20]);3console.log(word?.toUpperCase(), num?.toFixed(2));

T is a placeholder for "whatever type the array holds". The return type says it may be undefined for an empty array.

Here T is inferred as string, so word is string | undefined.

Here T is number. Same function, still fully typed.

Generic types#

Types can take parameters too. A result type that carries either data or an error is a classic example, and you will use it in the next lesson.

result.ts
type Result<T> = { ok: true; value: T } | { ok: false; error: string };function parseAge(input: string): Result<number> {  const age = Number(input);  if (!Number.isInteger(age) || age < 0) return { ok: false, error: `"${input}" is not an age` };  return { ok: true, value: age };}const result = parseAge("29");if (result.ok) console.log(result.value + 1); // 30else console.log(result.error);

Arrays are generic as well: string[] is shorthand for Array<string>, and Promise<User> is a promise that resolves to a User.

Constraints#

Sometimes a type parameter must have certain features. extends sets a minimum requirement.

constraints.ts
function longest<T extends { length: number }>(a: T, b: T): T {1  return a.length >= b.length ? a : b;}console.log(longest("kopi", "teh")); // "kopi"2console.log(longest([1, 2], [1, 2, 3])); // [1, 2, 3]

Any T is allowed as long as it has a numeric length: strings and arrays both qualify.

Passing a number would be an error, because numbers have no length.

keyof: safe property access#

keyof T is the union of a type's property names. Combined with a constraint, it lets you write a property getter that cannot ask for a field that does not exist.

pluck.ts
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {1  return items.map((item) => item[key]);}const people = [  { name: "Rina", age: 31 },  { name: "Dewa", age: 27 },];const names = pluck(people, "name"); // string[]const ages = pluck(people, "age"); // number[]console.log(names, ages);

K must be one of T's keys, and T[K] is the type of that property. pluck(people, "email") would not compile.

When not to use generics

If a type parameter appears only once, it adds nothing: function log<T>(value: T): void is just function log(value: unknown): void. Use a generic when it connects types, such as an input to an output. Over-generic code is harder to read than a little duplication.

ExerciseA typed last()

Write last<T>(items: T[]): T | undefined that returns the final element. Then call it with an array of booleans and check the inferred type.

Show solution
solution.ts
function last<T>(items: T[]): T | undefined {  return items[items.length - 1];}const flag = last([true, false, true]); // boolean | undefinedconsole.log(flag);

Recap#

  • Type parameters let one function or type work with many types while staying precise.
  • TypeScript usually infers them from arguments.
  • Constrain with extends; use keyof and T[K] for safe property access.