TypeScript Generics

Generics let you write code that works over many types while still tracking which type it's working with. A function that returns the first element of an array shouldn't return any. It should return a string for a string[] and a User for a User[]. Generics make that relationship explicit: function first<T>(xs: T[]): T.

They're the backbone of typed libraries: Array<T>, Promise<T>, React's useState<T>, TanStack Query's useQuery, and Zod's inferred schemas. Once you're comfortable reading and writing them, TypeScript goes from annotating code to designing APIs that are hard to misuse.

TL;DR

Quick Example

A typed groupBy and a type-safe property picker:

No explicit type arguments are needed. TypeScript infers T = User and K = "admin" | "member" from the call.

Core Concepts

Type Parameters and Inference

TypeScript infers type arguments from the values you pass. You write them explicitly only when there's nothing to infer from, for example useState<User | null>(null) or new Map<string, number>().

Constraints

Unconstrained T could be anything, so you can't access properties on it. extends narrows what's allowed:

The return type keeps the full type (Map<string, Product>), not just HasId. That's the advantage over declaring the parameter as HasId[].

keyof and Indexed Access

K extends keyof T constrains a key to the keys of an object type, and T[K] gives the type of the property at that key. Together they express "a key of this object and the value it points to":

Generic Interfaces, Types, and Classes

E = Error is a default: Result<User> means Result<User, Error>.

const Type Parameters

By default, TypeScript widens literals: ["a", "b"] is inferred as string[]. A const type parameter (TS 5.0+) keeps the literal types, which is ideal for config and route definitions:

Generics Plus Conditional and Mapped Types

Generics become far more expressive combined with conditional types (T extends string ? A : B), infer, and mapped types ({ [K in keyof T]: ... }). Those techniques are covered in TypeScript advanced features.

Designing Generic APIs

Best Practices

Name Parameters Meaningfully in Complex Signatures

T, K, and V are fine for short utilities. In signatures with several parameters, use TData, TError, TKey so readers (and error messages) stay clear.

Return the Narrowest Useful Type

function toArray<T>(x: T | T[]): T[] gives callers precise element types. Returning unknown[] pushes casts onto every caller.

Keep Casts Inside the Implementation

Sometimes the implementation needs an as (as with {} as Record<K, T[]> above) because TypeScript can't prove what you know. Contain it inside the function so the public signature stays sound, and test the function.

Test Types, Not Just Values

For library code, use type-level tests (expectTypeOf in Vitest, or // @ts-expect-error assertions) to lock in inference behavior.

Common Mistakes

Generics That Should Be unknown

Returning a Generic You Can't Actually Produce

A generic that exists only in the return type is a disguised type assertion. Validate untrusted data with a schema library such as Zod.

Over-Constraining

<T extends { id: string; name: string; email: string }> when the function only reads id forces callers to supply fields it never uses. Constrain to what you need.

FAQ

When should I use a generic instead of any?

Almost always. any switches off checking and loses the type. A generic preserves it: first<T>(xs: T[]): T returns a User for a User[], while first(xs: any[]): any returns something the compiler knows nothing about.

Why can't I access a property on T?

Because an unconstrained T could be any type, including number or null. Add a constraint (T extends { name: string }) so TypeScript knows the property exists.

What does T extends keyof U mean?

It restricts T to the union of U's property names. For interface User { id: number; name: string }, keyof User is "id" | "name", so T can only be one of those string literal types.

How do generics work with React components?

Components can be generic: function List<T>(props: { items: T[]; render: (item: T) => ReactNode }). TypeScript infers T from items, so render receives correctly typed items. In .tsx files, arrow functions need <T,> to avoid being parsed as JSX.

Related Topics

References