Skip to content

TypeScript Generics Explained: Functions for Types

Aman Kumar Singh5 min read
Part 2 of 9From the TypeScript Deep Dive series
TypeScript Generics Explained: Functions for Types — article by Aman Kumar Singh

Generics are the feature that people avoid the longest and regret avoiding the most. The angle brackets look intimidating, so a lot of TypeScript gets written with any in exactly the places generics belong, throwing away the type safety that was the whole point of using TypeScript. Once generics click, though, they stop looking like syntax and start looking like what they are: functions that operate on types instead of values. That one analogy carries most of the understanding.

This is part of the TypeScript type system series. Here we make generics concrete, from the basic idea through constraints and defaults.

The problem generics solve

Say you want a function that returns the first element of an array. Without generics, you are stuck making a bad trade. Type it for one element type and it only works for that:

function first(arr: number[]): number {
  return arr[0]
}

Type it loosely with any and it works for everything but tells you nothing:

function first(arr: any[]): any {
  return arr[0]
}
const name = first(["a", "b"]) // name is 'any' — no help, no safety

The information you lost is the relationship: the return type is the same as the array's element type. That relationship is exactly what a generic captures.

function first<T>(arr: T[]): T {
  return arr[0]
}
const name = first(["a", "b"]) // name is 'string'
const n = first([1, 2, 3])     // n is 'number'

The <T> introduces a type parameter — a placeholder for a type that gets filled in when the function is called. Read T as "whatever element type this array happens to have." The function now says "give me an array of some type T, and I will give you back a value of that same T." The caller does not even specify T; TypeScript infers it from the argument. You get full type safety and full reusability at once, which is the thing any could never give you.

Type parameters are function parameters for types

The mental model that unlocks generics: <T> is to types what (x) is to values. A regular function takes a value and returns a value. A generic takes a type and returns a value (or another type) that depends on it. T is not a special magic letter — it is just a parameter name, and you can name it anything. T is convention for a single generic, but a descriptive name is often clearer:

function wrapInArray<Item>(value: Item): Item[] {
  return [value]
}

This also demystifies where you have already seen generics. Array<string> is the generic Array type applied to string. Promise<User> is Promise applied to User. Map<string, number> takes two type parameters. Every one of those is the same idea: a type that is parameterized by another type, filled in at the point of use.

Constraints: generics that require some shape

A plain <T> means "absolutely any type," which is powerful but sometimes too permissive. Often you need T to have at least some structure — you want to accept many types, but only ones that have a length, or an id, or whatever your function actually touches. That is what extends does in a generic: it constrains the parameter to types assignable to a given shape.

function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b
}

longest("hello", "hi")       // works — strings have length
longest([1, 2], [3, 4, 5])   // works — arrays have length
longest(10, 20)              // error — numbers have no length

Here T extends { length: number } reads as "T can be any type, as long as it has a numeric length property." Inside the function you can safely access .length because the constraint guarantees it exists, and the return type still preserves the specific type the caller passed. Constraints are how you write generics that are flexible about the type but strict about the capability, which is usually exactly what real functions need. This is also the backbone of typed React component patterns and any utility that operates on "objects with an id."

A common pairing is constraining one parameter by another using keyof:

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

const user = { name: "Aman", age: 30 }
getProp(user, "name") // string
getProp(user, "age")  // number
getProp(user, "email") // error — "email" is not a key of user

K extends keyof T constrains the key to be one of T's actual property names, and the return type T[K] is the type of that specific property. You get a fully type-safe property accessor: valid keys only, and the exact value type for whichever key you pass. This pattern shows up constantly once you start looking for it.

Defaults and multiple parameters

Generics can take more than one parameter, and each can have a default, just like function arguments.

interface ApiResponse<TData, TError = string> {
  data?: TData
  error?: TError
}

type UserResponse = ApiResponse<User>          // TError defaults to string
type StrictResponse = ApiResponse<User, Error> // TError is Error

A default lets a generic stay convenient in the common case while remaining configurable when you need it. ApiResponse<User> works without specifying the error type because the default fills it in, but you can override it when a particular response uses a richer error shape.

When not to reach for generics

Generics earn their place when there is a genuine relationship between types to preserve — input to output, key to value, container to contents. That is the signal. If a function's input and output types are unrelated, a generic adds ceremony without value, and a plain type is clearer. The failure mode in both directions is worth naming: reaching for any where a generic would keep the relationship, and reaching for a generic where there is no relationship to keep. Ask whether two things in the signature must be the same type or related types. If yes, that is a generic. If no, it is not.

The reason generics matter so much is that they are the foundation everything else in the type system builds on. Conditional types, mapped types, and the entire library of utility types are all generics with extra machinery. Get comfortable reading and writing <T> and the advanced parts of TypeScript stop being a wall and start being a natural extension of something you already understand.

Key takeaways

  • A generic captures a relationship between types — like "the return type is the same as the array's element type" — that any and fixed types both throw away.
  • A type parameter <T> is to types what a value parameter (x) is to values: a placeholder filled in when the generic is used.
  • Constraints (T extends { length: number }) let a generic be flexible about the type but strict about the capability it requires.
  • K extends keyof T plus a T[K] return type gives a fully type-safe property accessor — valid keys only, exact value types.
  • Reach for a generic only when there is a genuine relationship between types to preserve; otherwise a plain type is clearer.

Frequently asked questions

What problem do generics solve?

They preserve the relationship between types that would otherwise be lost. A "return the first element" function needs its return type to match the array's element type. A fixed type only works for one element type; any works for all but gives no safety. A generic keeps both reusability and type safety.

What does <T> mean in TypeScript?

It declares a type parameter — a placeholder for a type that is filled in when the generic is used, usually inferred from the arguments. It works just like a function parameter, except it takes a type instead of a value. The name T is only a convention; any name works.

What are generic constraints?

A constraint written with extends limits a type parameter to types that have a required shape, like T extends { length: number }. Inside the generic you can safely use the guaranteed members, while still preserving the specific type the caller passed and rejecting types that lack the shape.

When should I not use a generic?

When there is no relationship between the input and output types to preserve. If two things in the signature must be the same type or related types, that is a generic. If they are unrelated, a generic just adds ceremony and a plain type is clearer.

Related articles

The satisfies Operator in TypeScript: Check Without Widening — Aman Kumar Singh
Mapped Types in TypeScript: Transforming Every Property — Aman Kumar Singh
TypeScript Type Narrowing and Type Guards — Aman Kumar Singh

Explore more on

Aman Kumar Singh

About the author

I'm Aman Kumar Singh, a software engineer in Noida, India building scalable full-stack products with React, Next.js, Node.js, NestJS, PostgreSQL, Redis, and AWS. I write about backend engineering, distributed systems, and system design.