Skip to main content

Generics: Reuse With Type Safety

intermediate15 min readLesson 88 of 143

Type parameters, inference at call sites, constraints with extends, and utility types you will actually use.

Generics are functions for types: one implementation, types vary per use, and the compiler tracks each use separately.

The idea

function first<T>(items: T[]): T | undefined {
  return items[0];
}
const a = first(["x", "y"]); // T inferred as string โ†’ a: string | undefined
const b = first([1, 2]); // T inferred as number โ†’ b: number | undefined

Call-site inference means you rarely write the angle brackets โ€” you get them from context.

Constraints

When the body needs capabilities beyond unknown, constrain:

function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
  return items.map((i) => i[key]);
}
pluck(users, "email"); // โœ“ โ€” and "emial" is a compile error

K extends keyof T ties the key to keys that actually exist on T โ€” a typo becomes a compile-time error instead of undefined at runtime.

Utility types

The built-ins cover 90% of needs:

  • Partial<T> โ€” all fields optional (patch/update payloads)
  • Pick<T, K> / Omit<T, K> โ€” select or remove fields (public view of a row)
  • ReturnType<typeof fn> โ€” capture what a function gives back
  • Record<K, V> โ€” typed dictionaries
type PublicUser = Omit<User, "passwordHash">;
function updateUser(id: number, patch: Partial<User>) { ... }

Use generics when the SHAPE relationship matters (in โ†’ out); use unknown when you truly accept anything.

Now practice

Generics โ€” PracticeShape-preserving functions: a first-or-default picker, a keyed group-by, and a result-wrapping combinator whose output shape mirrors its input.3 challenges ยท ยท ~18 min