ESC

Type to search the knowledge base.

Generics Basics

Type parameters for functions, components, and data structures — constraints, inference, and the mistakes that produce any-shaped APIs.

intermediate4 min read
  • typescript
  • generics
  • types

Generics let you write a function, type, or class that works over many types while preserving the relationship between inputs and outputs. Without them you either duplicate code or fall back to any / weak types.

Docs: TypeScript Handbook — Generics, keyof / indexed access.

Why generics exist

function firstNumber(arr: number[]): number | undefined {
  return arr[0];
}
function firstString(arr: string[]): string | undefined {
  return arr[0];
}

Same logic, different types. Generic version:

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

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

T is a type parameter — a placeholder filled by inference or explicit annotation.

Inference vs explicit arguments

first<string>(['x']); // explicit
first(['x']);         // inferred as string

Prefer inference when it works. Pass explicit type arguments when:

  1. The compiler can’t infer (empty arrays, complex conditional types).
  2. You want a wider or branded type than the literal inference.
  3. Public API clarity in awkward call sites.
const empty = first<number>([]); // T = number, result number | undefined

Constraints (extends)

Sometimes T can’t be anything — you need properties:

function pluckId<T extends { id: string }>(item: T): string {
  return item.id;
}

pluckId({ id: '1', name: 'Ada' }); // ok
// pluckId({ name: 'Ada' }); // error

Constraints are contracts on type parameters. Don’t over-constrain — if you only need id, don’t require a full User interface when a structural { id: string } suffices.

keyof and indexed access

function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: '1', age: 42 };
const age = getProp(user, 'age'); // number
// getProp(user, 'nope'); // error

This pattern is the foundation of typed selectors, form libraries, and path helpers.

Multiple type parameters

function mapValues<T extends object, U>(
  obj: T,
  fn: (value: T[keyof T], key: keyof T) => U,
): Record<keyof T, U> {
  const out = {} as Record<keyof T, U>;
  (Object.keys(obj) as Array<keyof T>).forEach((k) => {
    out[k] = fn(obj[k], k);
  });
  return out;
}

Name parameters meaningfully when several exist (TItem, TError) — single-letter is fine for short helpers.

Generics on types and interfaces

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

type ApiResponse<T> = {
  data: T;
  updatedAt: string;
};

interface Repository<T, TId extends string | number = string> {
  get(id: TId): Promise<T | null>;
  save(entity: T): Promise<void>;
}

Default type parameters (E = Error) keep call sites clean while allowing customization.

Generic functions in React (sketch)

type ListProps<T> = {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyFn: (item: T) => string;
};

function List<T>({ items, renderItem, keyFn }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={keyFn(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Usage — T inferred from items
<List
  items={users}
  keyFn={(u) => u.id}
  renderItem={(u) => u.name}
/>

Arrow components + generics can be awkward in TSX (const List = <T,>(…) needs a trailing comma). function declarations often parse more cleanly.

Common mistakes

1. Fake generics that don’t relate types

// Bad: T never used in a meaningful relationship
function parse<T>(raw: string): T {
  return JSON.parse(raw) as T;
}

This is an assertion in a costume. Prefer unknown + validation, or a generic that depends on a schema argument.

2. Over-using T extends any / unconstrained escape

If you need “accept anything, return the same,” use generics; if you need “accept anything, I will narrow,” use unknown.

3. Nested generics unreadable

Split intermediate types:

type UserDto = { id: string; email: string };
type Page<T> = { items: T[]; nextCursor?: string };
type UserPage = Page<UserDto>;

4. Forcing type arguments everywhere

Fight the compiler with better inference (from arguments) before adding noise at every call.

5. Confusing value space and type space

const enumOrArray = …;
type T = typeof enumOrArray; // type from value

Generics operate on types. Runtime arrays don’t automatically become type parameters without as const / typeof patterns.

Defaults vs inference interaction

function createState<T = string>(initial?: T) {
  return { value: initial as T };
}

Defaults apply when inference can’t run. Be careful: a default of string may “win” when you expected a broader inference — test empty call sites.

Interview angle

Explain “preserve input/output relationship.” Write first<T> and getProp with keyof. Call out why parse<T>(string): T is unsafe. Mention constraints and React list component generics.

Live coding: implement Promise.all-like typing for a tuple of promises, or a typed pick(obj, keys).

Further reading

Related guides