Learn / Programming / TypeScript / Generics and Utility Types

Intermediate 17 min

Generics and Utility Types

Write reusable typed functions and transform types with Partial, Pick and more.

What you will learn

  • Write a generic function
  • Use utility types
  • Constrain generics

Sometimes the same logic works for many types: returning the first item of any array, wrapping any value in a response. Generics let you write that code once while keeping full type safety, rather than falling back to any.

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

const n = first([1, 2, 3]);        // number | undefined
const s = first(["a", "b"]);       // string | undefined

T is a type parameter, a placeholder filled in from the arguments. The compiler remembers that numbers went in, so numbers come out.

interface ApiResponse<T> {
  data: T;
  error: string | null;
}

interface Product { id: number; title: string; }

const res: ApiResponse<Product[]> = {
  data: [{ id: 1, title: "Pen" }],
  error: null,
};

Constraints

Use extends to require that a type parameter has certain properties.

function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

longest("apple", "fig");        // works for strings
longest([1, 2], [1, 2, 3]);     // and arrays
// longest(1, 2);               // Error: number has no length
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
getProp({ id: 1, name: "Ada" }, "name");   // string
// getProp({ id: 1 }, "email");            // Error: not a key

Utility types

TypeScript ships helpers that build new types from existing ones.

interface User { id: number; name: string; email: string; }

type Draft     = Partial<User>;                 // every field optional
type Required_ = Required<User>;                // every field required
type Preview   = Pick<User, "id" | "name">;     // only these keys
type NoEmail   = Omit<User, "email">;           // all but this key
type Roles     = Record<string, "admin" | "user">;   // map of string keys
type Frozen    = Readonly<User>;

function updateUser(id: number, changes: Partial<User>) { /* ... */ }
const config = { host: "localhost", port: 5432 };
type Config = typeof config;          // { host: string; port: number }

async function load() { return { id: 1, name: "Ada" }; }
type Loaded = Awaited<ReturnType<typeof load>>;   // { id: number; name: string }
Keep it readable

Generics are powerful but can become cryptic. If a type takes minutes to understand, consider simplifying it, or splitting it into named pieces.

Try it yourself

Write a generic groupBy<T>(items: T[], key: keyof T) returning a record from key value to arrays of items.

Show solution
function groupBy<T>(items: T[], key: keyof T): Record<string, T[]> {
  const out: Record<string, T[]> = {};
  for (const item of items) {
    const k = String(item[key]);
    (out[k] ??= []).push(item);
  }
  return out;
}

groupBy([{ t: "a", n: 1 }, { t: "a", n: 2 }, { t: "b", n: 3 }], "t");