Generic Functions¶
A generic function is a function that takes a type parameter—a placeholder for a type that will be provided later.
Think of generics as “functions for types.”
Type Parameters¶
A type parameter is written inside angle brackets:
function identity<T>(value: T): T {
return value
}
Here:
Tis a type parametervalue: Tmeans the function accepts any type- the return type is the same type
Usage¶
identity(10) // T = number
identity("hello") // T = string
identity({ x: 1 }) // T = { x: number }
TypeScript infers the type parameter automatically.
Explicit type parameter (rarely needed)¶
identity<number>(42)
Why Generics Matter¶
Without generics:
function wrap(value: any): any {
return value
}
You lose all type information.
With generics:
function wrap<T>(value: T): T {
return value
}
You preserve the exact type.
Real‑World Example: Generic Array Helpers¶
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
const u = first([{ id: 1 }]) // { id: number } | undefined
Generics let TypeScript “follow” the type through the function.
Constraints (extends)¶
Sometimes you want a generic function to accept any type…
…but only if it meets certain requirements.
That’s where constraints come in.
Example: Require an id property¶
function getId<T extends { id: string | number }>(item: T) {
return item.id
}
Now this works:
getId({ id: 123, name: "Alice" })
But this does not:
getId({ name: "Bob" }) // ❌ Error: missing id
Why constraints matter¶
They let you:
- enforce structure
- prevent invalid inputs
- write safer, more expressive APIs
Multiple Constraints¶
You can combine constraints using intersections:
function logPosition<T extends { x: number } & { y: number }>(p: T) {
console.log(p.x, p.y)
}
Or extend other type parameters:
function compare<T extends U, U>(a: T, b: U) {
return a === b
}
Default Type Parameters¶
Just like function parameters, type parameters can have defaults.
Example: Default to string¶
function createMap<T = string>() {
return new Map<string, T>()
}
Usage:
const m1 = createMap() // Map<string, string>
const m2 = createMap<number>() // Map<string, number>
Why defaults matter¶
- reduce noise
- improve readability
- allow flexible APIs
Real‑World Example: Fetch Wrapper¶
async function fetchJSON<T = unknown>(url: string): Promise<T> {
const res = await fetch(url)
return res.json() as T
}
Usage:
type User = { id: number; name: string }
const user = await fetchJSON<User>("/api/user")
Or rely on the default:
const data = await fetchJSON("/api/anything") // T = unknown
This pattern is used everywhere in modern TypeScript codebases.
Putting It All Together¶
Here’s a generic function with:
- a type parameter
- a constraint
- a default type parameter
function pluck<T extends object, K extends keyof T = keyof T>(
obj: T,
key: K
): T[K] {
return obj[key]
}
const user = { id: 1, name: "Alice" }
const name = pluck(user, "name") // string
const id = pluck(user, "id") // number
This is the kind of expressive, type‑safe API generics make possible.
Summary¶
In this lesson, you learned the core building blocks of generic functions:
1. Type parameters¶
- placeholders for types
- inferred automatically
- preserve type information
2. Constraints (extends)¶
- restrict what types are allowed
- enforce structure
- prevent invalid inputs
3. Default type parameters¶
- reduce boilerplate
- improve API ergonomics
- allow flexible usage
Generics are the foundation of advanced TypeScript.
Everything from React hooks to utility libraries to state machines relies on them.