TypeScript Generics
Generics let you write one function or type that works safely with any type. This guide starts from zero and builds up to real patterns used in production code.
Section
What Are Generics?
Generics solve the copy-paste problem write one function or type that works safely with any type, instead of one version per type.
5 concepts
// Problem: without generics you copy-paste the same logic per type
function wrapString(value: string): { value: string } { return { value } }
function wrapNumber(value: number): { value: number } { return { value } }
function wrapBoolean(value: boolean): { value: boolean } { return { value } }
// Solution: one generic function covers every type
function wrap<T>(value: T): { value: T } {
return { value }
}
const a = wrap("hello") // { value: string }
const b = wrap(42) // { value: number }
const c = wrap(true) // { value: boolean }
const d = wrap([1, 2, 3]) // { value: number[] }
Concept
Type Parameter
A placeholder for a type, written as <T> after a function or type name. TypeScript fills it in based on what you pass.
Example: In identity<T>(value: T): T, the T is the type parameter it becomes string when you call identity("hello").
Concept
Type Argument
The actual type you supply when calling a generic. You can write it explicitly as identity<string>("hello") or let TypeScript infer it.
Example: Calling wrap<number>(42) passes number as the type argument, so the return type becomes number.
Concept
Type Inference
TypeScript's ability to figure out the type argument from the value you pass, so you rarely need to write <T> explicitly at call sites.
Example: Calling identity("hello") TypeScript sees a string argument and automatically infers T = string.
Concept
Constraint
A restriction on what types T can be, written as T extends SomeType. It narrows what T can be so you can safely access properties on it.
Example: <T extends { length: number }> means T must have a length property arrays and strings both qualify.
Concept
Default Type Parameter
A fallback type used when no type argument is supplied, written as <T = DefaultType>. Works like a default function argument.
Example: <T = string> means callers can omit the type argument and T will be string unless they specify otherwise.
Section
Generic Functions
Put <T> after the function name. TypeScript infers the type argument from what you pass in you almost never need to write the angle brackets at the call site.
4 examples
Example
Identity the simplest generic
The identity function returns whatever you give it. Without generics it forces you to choose any (losing type safety) or write one version per type. With T, the input and output types are linked.
function identity<T>(value: T): T {
return value
}
// TypeScript infers T from the argument no need to write <string>
const name = identity("Alice") // string
const age = identity(30) // number
const flag = identity(false) // boolean
// You can also be explicit when inference needs a nudge
const result = identity<string[]>(["a", "b"]) // string[]
Example
Generic array utilities
Generic functions shine when working with arrays. The element type flows through the whole function without losing precision.
// Return the first element, or undefined if empty
function first<T>(arr: T[]): T | undefined {
return arr[0]
}
const firstNum = first([10, 20, 30]) // number | undefined
const firstWord = first(["a", "b"]) // string | undefined
// Return last element
function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1]
}
// Chunk an array into groups of n
function chunk<T>(arr: T[], size: number): T[][] {
const result: T[][] = []
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size))
}
return result
}
chunk([1, 2, 3, 4, 5], 2) // number[][] → [[1,2],[3,4],[5]]
Example
Transforming with two type parameters
When the input and output types differ, use two type parameters. This is how the built-in Array.map works internally.
// Transform every element from type A to type B
function mapArray<A, B>(arr: A[], transform: (item: A) => B): B[] {
return arr.map(transform)
}
const lengths = mapArray(["hello", "world"], (s) => s.length)
// number[] → [5, 5]
const doubled = mapArray([1, 2, 3], (n) => n * 2)
// number[] → [2, 4, 6]
const labels = mapArray([1, 2, 3], (n) => `item-${n}`)
// string[] → ["item-1", "item-2", "item-3"]
Example
Type-safe filter with type predicates
Generic functions can work with type predicates, making type guards reusable for any type.
function isDefined<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined
}
const items = [1, null, 2, undefined, 3]
const defined = items.filter(isDefined)
// number[] → [1, 2, 3]
// Works the same with strings
const words = ["hello", null, "world", undefined]
const realWords = words.filter(isDefined)
// string[] → ["hello", "world"]
Section
Generic Interfaces and Types
Describe reusable shapes API responses, form fields, repository contracts where the core structure stays the same but the data type changes.
4 examples
Example
Generic interface Box<T>
Interfaces can carry a type parameter so every property that references T stays in sync. The interface defines the shape; the type argument fills in the blanks.
interface Box<T> {
value: T
label: string
}
const numberBox: Box<number> = { value: 42, label: "age" }
const stringBox: Box<string> = { value: "Alice", label: "name" }
// TypeScript catches mismatches at compile time
const wrong: Box<number> = { value: "oops", label: "x" }
// Error: Type 'string' is not assignable to type 'number'
Example
Generic type alias ApiResponse<T>
A type alias with a generic lets you describe a consistent API envelope shape while keeping the data field typed to the actual payload.
type ApiResponse<T> = {
data: T
status: number
message: string
}
type User = { id: number; name: string }
type Post = { id: number; title: string; body: string }
const userResponse: ApiResponse<User> = {
data: { id: 1, name: "Alice" },
status: 200,
message: "ok",
}
const postResponse: ApiResponse<Post[]> = {
data: [{ id: 1, title: "Hello", body: "World" }],
status: 200,
message: "ok",
}
Example
Pair<A, B> composable generic tuples
Generic type aliases compose. A Pair<A, B> holds two related values of different types without losing type information.
type Pair<A, B> = { first: A; second: B }
// Same types
const point: Pair<number, number> = { first: 3, second: 4 }
// Different types, still safe
const entry: Pair<string, number> = { first: "Alice", second: 30 }
// Nest generics for layered shapes
type Labeled<T> = Pair<string, T>
const labeled: Labeled<boolean> = { first: "active", second: true }
Example
Generic interface extending another
Generic interfaces can extend other generics for example, a repository contract that works for any identifiable entity.
interface Identifiable {
id: string
}
// T must have an id field (constrained by Identifiable)
interface Repository<T extends Identifiable> {
findById(id: string): T | undefined
findAll(): T[]
save(item: T): void
delete(id: string): void
}
type Product = { id: string; name: string; price: number }
declare const productRepo: Repository<Product>
const p = productRepo.findById("abc") // Product | undefined
Section
Generic Classes
Classes take the type parameter in their declaration. Every method is then typed to whatever you pass when you instantiate.
2 examples
Example
Generic Stack<T>
A stack is the classic generic class example. Every push and pop is typed to whatever T you instantiate with completely type-safe, zero duplication.
class Stack<T> {
private items: T[] = []
push(item: T): void {
this.items.push(item)
}
pop(): T | undefined {
return this.items.pop()
}
peek(): T | undefined {
return this.items[this.items.length - 1]
}
get size(): number { return this.items.length }
isEmpty(): boolean { return this.items.length === 0 }
}
const numStack = new Stack<number>()
numStack.push(1)
numStack.push(2)
numStack.pop() // number | undefined → 2
const strStack = new Stack<string>()
strStack.push("hello")
strStack.peek() // string | undefined → "hello"
Example
Generic TypedEmitter<Events>
Type-safe event emitters use generics to map event names to their payload types. No more guessing what a "data" event carries.
type EventMap = Record<string, unknown>
class TypedEmitter<Events extends EventMap> {
private listeners: {
[K in keyof Events]?: Array<(payload: Events[K]) => void>
} = {}
on<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void {
if (!this.listeners[event]) this.listeners[event] = []
this.listeners[event]!.push(listener)
}
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
this.listeners[event]?.forEach((fn) => fn(payload))
}
}
type AppEvents = {
userJoined: { userId: string; name: string }
messageReceived: { from: string; text: string }
}
const emitter = new TypedEmitter<AppEvents>()
emitter.on("userJoined", (payload) => {
console.log(`${payload.name} joined`) // fully typed
})
emitter.emit("userJoined", { userId: "u1", name: "Alice" })
// emitter.emit("userJoined", { bad: "data" }) // Error!
Section
Constraints with extends
By default TypeScript won't let you access any property on T. Write T extends SomeType to say T must have at least this shape then you can use those properties safely.
4 examples
Example
extends basic shape constraint
Without constraints, TypeScript won't let you access any property on T. With T extends { length: number }, you tell TypeScript that T definitely has a length so you can use it safely.
// Without constraint: TypeScript can't know T has .length
function getLength<T>(value: T): number {
return value.length // Error: Property 'length' does not exist on type 'T'
}
// With constraint: T must have a length property
function getLength<T extends { length: number }>(value: T): number {
return value.length // Safe!
}
getLength("hello") // 5 string has length
getLength([1, 2, 3]) // 3 array has length
getLength({ length: 7 }) // 7 plain object works too
// getLength(42) // Error: number has no length
Example
keyof constraint safe property access
K extends keyof T constrains K to only be keys that actually exist on T. This is the pattern behind Omit, Pick, and property accessor helpers.
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { id: 1, name: "Alice", active: true }
const name = getProperty(user, "name") // string
const id = getProperty(user, "id") // number
const active = getProperty(user, "active") // boolean
// TypeScript rejects keys that don't exist
// getProperty(user, "email") // Error: not assignable to keyof typeof user
Example
extends union restricting to primitive types
Constrain T to a union of types when a function only makes sense for certain primitives.
function sortArray<T extends string | number>(arr: T[]): T[] {
return [...arr].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
}
sortArray([3, 1, 2]) // number[] → [1, 2, 3]
sortArray(["c", "a", "b"]) // string[] → ["a", "b", "c"]
// sortArray([true, false]) // Error: boolean doesn't satisfy constraint
Example
Intersection constraints multiple requirements
Constraints can combine with intersection types to require multiple capabilities at once.
interface HasId { id: string }
interface HasTimestamps { createdAt: Date; updatedAt: Date }
// T must have both id and timestamps
function formatRecord<T extends HasId & HasTimestamps>(record: T): string {
return `[${record.id}] created ${record.createdAt.toISOString()}`
}
type Post = HasId & HasTimestamps & { title: string }
const post: Post = {
id: "p1",
title: "Hello",
createdAt: new Date("2024-01-01"),
updatedAt: new Date("2024-01-02"),
}
formatRecord(post) // "[p1] created 2024-01-01T00:00:00.000Z"
Section
Default Type Parameters
Write <T = string> and callers can skip the angle brackets entirely. TypeScript uses the default automatically, just like a default function argument.
2 examples
Example
Default type parameter T = string
A default lets callers omit the angle brackets when the default type is good enough, while still allowing explicit override when needed.
interface FormField<T = string> {
value: T
label: string
required: boolean
}
// No need to write <string> it's the default
const nameField: FormField = {
value: "Alice",
label: "Name",
required: true,
}
// Override the default when needed
const ageField: FormField<number> = {
value: 30,
label: "Age",
required: false,
}
const activeField: FormField<boolean> = {
value: true,
label: "Active",
required: true,
}
Example
Default with constraint T extends X = DefaultX
Defaults and constraints can combine. The default must itself satisfy the constraint.
type Serializable = string | number | boolean | null
// T must be Serializable, defaults to string
function serialize<T extends Serializable = string>(value: T): string {
return JSON.stringify(value)
}
serialize("hello") // works, T = string (default)
serialize(42) // works, T = number (inferred)
serialize(true) // works, T = boolean (inferred)
// Defaults in type aliases work the same way
type Cache<K extends string = string, V = unknown> = Map<K, V>
const generic: Cache = new Map() // Map<string, unknown>
const specific: Cache<string, number> = new Map() // Map<string, number>
Section
Multiple Type Parameters
Separate multiple parameters with commas: <A, B>. Each is independent TypeScript tracks them separately. Two parameters cover most real-world cases.
3 examples
Example
Result<T, E> typed success and failure
A Result type with two parameters gives you typed success values and typed errors in one shape, avoiding untyped try/catch blocks.
type Result<T, E = Error> =
| { success: true; value: T }
| { success: false; error: E }
function divide(a: number, b: number): Result<number, string> {
if (b === 0) return { success: false, error: "Division by zero" }
return { success: true, value: a / b }
}
const result = divide(10, 2)
if (result.success) {
console.log(result.value) // number
} else {
console.log(result.error) // string
}
Example
zip<A, B> pairing two arrays
Two type parameters let you pair elements from arrays of different types, keeping each element's type precise in the output tuple.
function zip<A, B>(arrA: A[], arrB: B[]): [A, B][] {
const length = Math.min(arrA.length, arrB.length)
return Array.from({ length }, (_, i) => [arrA[i], arrB[i]])
}
const names = ["Alice", "Bob", "Carol"]
const scores = [95, 87, 92]
const pairs = zip(names, scores)
// [string, number][] → [["Alice", 95], ["Bob", 87], ["Carol", 92]]
const flags = zip([1, 2, 3], [true, false, true])
// [number, boolean][]
Example
fromEntries<K, V> typed Object.fromEntries
Two type parameters give precise key and value types to a dictionary builder, improving on the loose return type of the native Object.fromEntries.
function fromEntries<K extends string, V>(
entries: ReadonlyArray<readonly [K, V]>
): Record<K, V> {
return Object.fromEntries(entries) as Record<K, V>
}
const entries = [
["alice", 95],
["bob", 87],
] as const
const scores = fromEntries(entries)
// Record<"alice" | "bob", number>
// scores.alice → number
// scores.unknown // Error: key doesn't exist
Section
Built-in Utility Types
TypeScript ships with generic utility types in the standard library each one takes a type parameter and transforms it. Knowing these saves you from reimplementing common transformations.
4 examples
Example
Partial<T> and Required<T>
Partial<T> makes all properties optional useful for update payloads. Required<T> does the opposite, removing all optionals.
type User = {
id: number
name: string
email?: string
age?: number
}
type UserUpdate = Partial<User>
// { id?: number; name?: string; email?: string; age?: number }
type FullUser = Required<User>
// { id: number; name: string; email: string; age: number }
function updateUser(id: number, changes: Partial<User>): void {
// Only the provided fields are updated
}
updateUser(1, { name: "Bob" }) // valid
updateUser(1, { name: "Bob", age: 31 }) // valid
Example
Pick<T, K> and Omit<T, K>
Pick keeps only the keys you list. Omit removes the keys you list. Both use generics internally: Pick<T, K extends keyof T>.
type User = {
id: number
name: string
email: string
passwordHash: string
createdAt: Date
}
// Only expose safe fields to the frontend
type PublicUser = Omit<User, "passwordHash">
// { id: number; name: string; email: string; createdAt: Date }
// Just the fields needed for a summary card
type UserSummary = Pick<User, "id" | "name">
// { id: number; name: string }
// Combine for layered shapes
type EditableUser = Pick<Partial<User>, "name" | "email">
// { name?: string; email?: string }
Example
Record<K, V>
Record<K, V> creates an object type where all keys are type K and all values are type V. Better inference than { [key: string]: V }.
type Status = "active" | "inactive" | "pending"
const statusConfig: Record<Status, { label: string; color: string }> = {
active: { label: "Active", color: "green" },
inactive: { label: "Inactive", color: "gray" },
pending: { label: "Pending", color: "yellow" },
// TypeScript errors if you miss any key or add an unknown one
}
type CountryCode = "US" | "GB" | "DE"
const dialCodes: Record<CountryCode, string> = {
US: "+1",
GB: "+44",
DE: "+49",
}
Example
ReturnType<F> and Parameters<F>
ReturnType<F> extracts the return type of a function. Parameters<F> extracts the parameter tuple. Both use infer internally.
function fetchUser(id: number): Promise<{ name: string; email: string }> {
return Promise.resolve({ name: "Alice", email: "a@example.com" })
}
type FetchUserResult = ReturnType<typeof fetchUser>
// Promise<{ name: string; email: string }>
type FetchUserParams = Parameters<typeof fetchUser>
// [id: number]
// Preserve types when wrapping functions
function withLogging<F extends (...args: unknown[]) => unknown>(
fn: F
): (...args: Parameters<F>) => ReturnType<F> {
return (...args) => {
console.log("calling", fn.name, args)
return fn(...args) as ReturnType<F>
}
}
Section
Real-World Patterns
Patterns that show up constantly in production TypeScript each one combines constraints, multiple type parameters, and defaults into a single reusable building block.
3 examples
Example
useFetch<T> typed data fetching hook
A generic fetch hook gives every API call its own typed result without duplicating the loading/error/data state logic.
import { useState, useEffect } from "react"
type FetchState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error }
function useFetch<T>(url: string): FetchState<T> {
const [state, setState] = useState<FetchState<T>>({ status: "idle" })
useEffect(() => {
setState({ status: "loading" })
fetch(url)
.then((res) => res.json() as Promise<T>)
.then((data) => setState({ status: "success", data }))
.catch((error: Error) => setState({ status: "error", error }))
}, [url])
return state
}
type Post = { id: number; title: string; body: string }
function PostPage({ id }: { id: number }) {
const result = useFetch<Post>(`/api/posts/${id}`)
if (result.status === "loading") return <p>Loading...</p>
if (result.status === "error") return <p>{result.error.message}</p>
if (result.status === "success") return <h1>{result.data.title}</h1>
return null
}
Example
createStore<S> tiny typed state manager
Generic state managers keep the store shape and the action handlers in sync. No any required anywhere in the implementation.
type StoreListener<S> = (state: S) => void
function createStore<S>(initialState: S) {
let state = initialState
const listeners = new Set<StoreListener<S>>()
function getState(): S { return state }
function setState(updater: S | ((prev: S) => S)): void {
state = typeof updater === "function"
? (updater as (prev: S) => S)(state)
: updater
listeners.forEach((fn) => fn(state))
}
function subscribe(listener: StoreListener<S>): () => void {
listeners.add(listener)
return () => listeners.delete(listener)
}
return { getState, setState, subscribe }
}
type CartState = { items: string[]; total: number }
const cartStore = createStore<CartState>({ items: [], total: 0 })
cartStore.subscribe((state) => console.log("cart updated", state))
cartStore.setState((prev) => ({
items: [...prev.items, "Widget"],
total: prev.total + 9.99,
}))
Example
parseWith<T> validated parsing with a schema
A generic parse helper decouples validation logic from types. The schema function acts as both a validator and a type guard.
type Parser<T> = (raw: unknown) => T
type Result<T, E> = { success: true; value: T } | { success: false; error: E }
function parseWith<T>(raw: unknown, parser: Parser<T>): Result<T, string> {
try {
return { success: true, value: parser(raw) }
} catch (err) {
return { success: false, error: String(err) }
}
}
type User = { id: number; name: string }
function parseUser(raw: unknown): User {
if (
typeof raw !== "object" || raw === null ||
typeof (raw as Record<string, unknown>).id !== "number" ||
typeof (raw as Record<string, unknown>).name !== "string"
) {
throw new Error("Invalid user shape")
}
return raw as User
}
const result = parseWith(await response.json(), parseUser)
if (result.success) {
console.log(result.value.name) // string fully typed
} else {
console.error(result.error) // string
}
Reference
Quick reference when to use what
- Generic function: same logic, different types
wrap<T>,first<T>,mapArray<A, B> - Generic interface/type: reusable shape with a variable slot
ApiResponse<T>,Result<T, E> - Generic class: data structure that works for any type
Stack<T>,TypedEmitter<Events> - Constraint (extends): you need to access a property on T
T extends { length: number } - keyof constraint: safe property access:
K extends keyof T - Default type parameter: most callers use the same type
<T = string> - Utility types: transform existing types without rewriting them
Partial<T>,Pick<T, K>,Record<K, V>