Skip to content

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:

  • T is a type parameter
  • value: T means 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.