Generics: Functions, Types, Interfaces, and Classes

Build a complete, working understanding of generics — from a single generic function to generic interfaces, classes, multiple type parameters, constraints with extends and keyof, default type parameters, and where inference can fail — so you can design reusable, fully type-safe APIs.

Generics are TypeScript's tool for writing code that works across many types without giving up type safety. Instead of choosing between a rigid function locked to one type and a loose function typed with any, a generic function stays flexible while TypeScript tracks exactly which type was used at each call site.

Generic motivation and generic functions

Consider a function that simply returns whatever you pass it. Typed with any, it compiles, but all type information is destroyed — the caller gets back any regardless of what went in, losing autocomplete and every later type check. A generic type parameter, written <T>, preserves that connection: function identity<T>(value: T): T { return value; } means whatever type goes in comes back out, exactly.

TypeScript infers T from the call, so you almost never write the type argument explicitly. identity(5) infers T = number, and identity("hello") infers T = string — each call site gets a precisely and independently typed result, with zero extra annotation.

Generic types, interfaces, and classes

A type alias or interface can itself be generic, parameterizing over the type it holds: interface Box<T> { value: T; } describes a box that can hold any type, and Box<string> and Box<number> are two distinct, fully-typed instantiations of the same shape. This is exactly how built-in types like Array<T> and Promise<T> are defined.

Generic classes work the same way: class Stack<T> { private items: T[] = []; push(item: T): void { this.items.push(item); } pop(): T | undefined { return this.items.pop(); } }. A new Stack<number>() only accepts and returns numbers from push/pop, while new Stack<string>() is a completely separately-typed instance of the same reusable class definition.

Generic interfaces are especially common for describing callback shapes and API response envelopes: interface ApiResponse<T> { data: T; status: number; } lets you write one interface that correctly types ApiResponse<User>, ApiResponse<Product[]>, and any other response shape your API returns, without duplicating the status field's definition each time.

Multiple type parameters and constraints

A generic can take more than one type parameter: function pair<A, B>(first: A, second: B): [A, B] { return [first, second]; } links two independently-inferred types together in one return value — pair(1, "a") infers A = number, B = string and returns a precisely-typed tuple [number, string].

A constraint restricts what a generic type parameter is allowed to be, using extends. function getId<T extends { id: number }>(item: T): number { return item.id; } requires T to at least have an id property, without pinning the function to one exact object shape — getId({ id: 1, name: "Ada" }) works because the passed object satisfies the constraint, while getId({ name: "Ada" }) fails to compile because it's missing id.

A keyof constraint is especially powerful for building type-safe property-access utilities: function getProp<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; }. Here K is constrained to only the actual property names of T, so getProp(user, "email") type-checks and returns exactly the type of user.email, while getProp(user, "nonexistent") fails to compile because "nonexistent" isn't a key of T.

Default parameters, inference limits, and where you'll see this next

A generic type parameter can have a default, just like a function parameter: interface ApiResponse<T = unknown> { data: T; status: number; } lets you write ApiResponse on its own (defaulting T to unknown) when you don't yet know or don't care about the payload shape, while still supporting ApiResponse<User> when you do.

Inference isn't magic — it can fail or produce a wider type than you want when there's nothing concrete for TypeScript to infer from, such as calling a generic function with no arguments at all, or passing an empty array literal. In those cases TypeScript often falls back to unknown or infers the narrowest common type across mismatched arguments, and you may need to supply the type argument explicitly, like identity<string>(), to get the result you intended — this is one of the more common places beginners see a confusing inferred type and need to read the compiler's diagnostic carefully to understand what it inferred and why.

Newer TypeScript versions also support const type parameters (function tuple<const T extends readonly unknown[]>(...items: T): T), which preserve literal types instead of widening them — useful for APIs that want to keep exact literal values rather than the general string/number types. You'll also encounter generics constantly once you reach the React course: generic components (function List<T>(props: { items: T[]; renderItem: (item: T) => ReactNode })) and generic hooks (a custom useFetch<T>(url: string): { data: T | null }) are everywhere in typed React code — full depth on those patterns comes later, but the underlying generic mechanics are exactly what you've learned in this lesson.

Generic declaration forms

Type parameter names are conventionally single capital letters (T, K, V) but can be any identifier. K extends keyof T restricts K to the literal property names that actually exist on T. A default type parameter (T = unknown) only applies when the type argument is omitted entirely.

// Generic function
function identity<T>(value: T): T {
  return value;
}

// Constrained generic function
function getId<T extends { id: number }>(item: T): number {
  return item.id;
}

// Generic interface
interface Box<T> {
  value: T;
}

// Generic class
class Stack<T> {
  private items: T[] = [];
  push(item: T): void { this.items.push(item); }
  pop(): T | undefined { return this.items.pop(); }
}

// Multiple type parameters
function pair<A, B>(first: A, second: B): [A, B] {
  return [first, second];
}

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

// Default type parameter
interface ApiResponse<T = unknown> {
  data: T;
  status: number;
}

Code Examples

TypeScript infers a different, correctly specific T at each call site, so a is typed number and b is typed string — neither becomes any, and neither required an explicit type argument. Expected result: a: number = 5, b: string = "hello"

The extends { id: number } constraint requires whatever T is to at least have an id property, without pinning the function to one exact object shape.

Common mistake: Writing an unconstrained generic like function getId<T>(item: T) and then trying to access item.id inside the function body is a compile error — TypeScript has no guarantee T has an id property at all.

Stack<number> locks that instance's push/pop to numbers only. getProp uses K extends keyof T so the second argument is checked against the actual property names of the first — getProp(user, "email") correctly infers a string result and rejects made-up keys at compile time.

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

function identity<T>(value: T): T {
  return value;
}

const a = identity(5);        // T inferred as number
const b = identity("hello");  // T inferred as string
function getId<T extends { id: number }>(item: T): number {
  return item.id;
}

getId({ id: 1, name: "Ada" }); // OK
getId({ name: "Ada" });        // Error: missing 'id'
class Stack<T> {
  private items: T[] = [];
  push(item: T): void { this.items.push(item); }
  pop(): T | undefined { return this.items.pop(); }
}

const numbers = new Stack<number>();
numbers.push(1);
numbers.push(2);
// numbers.push("3"); // Error: string is not assignable to number

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

const user = { id: 1, email: "[email protected]" };
const email = getProp(user, "email"); // typed as string
// getProp(user, "missing");           // Error: 'missing' is not keyof typeof user

Type flows through the generic

The same generic function body adapts its checked types per call, instead of one fixed signature or an unsafe any.

identity(5)                       getId({ id: 1, name: "Ada" })
     |                                        |
  T = number (inferred)              T = { id: number; name: string }
     |                                (satisfies extends { id: number })
  return type = T = number                    |
     |                                  return type = number
  result: number                              |
                                        result: 1

Important Rules

Reach for a generic when a function's or type's input and output are related but not fixed — if the types are unrelated, plain overloads or a union type is usually clearer.

Add a constraint (extends) whenever the function body needs to access a property or call a method on the generic value.

Use K extends keyof T whenever a function's second parameter should be restricted to the real property names of its first parameter.

Prefer inferred type arguments; only specify them explicitly when TypeScript truly can't infer them from the call, or infers something wider than you intended.

Give a generic interface or type a sensible default (T = unknown, not T = any) when it's reasonable for callers to omit the type argument.

Where this shows up

Generics power nearly every reusable utility in a real TypeScript codebase: array helpers like a custom first<T>(arr: T[]): T | undefined, API client wrappers that preserve the response payload type end to end, state containers and stores that stay typed to whatever data they hold, and — once you reach the React course — generic components like a typed <List<T> items={...} renderItem={...} /> and generic hooks like useFetch<T>(url) that return correctly-typed data for whatever endpoint you call it on.

function identity(value: any): any { return value; } function identity<T>(value: T): T { return value; }

Using any compiles but throws away all type information; the caller's specific type is lost both coming in and going out, unlike a generic which preserves it exactly.

[multiple-choice] What is the main advantage of a generic function over one typed with any? A. Generics run faster at runtime B. Generics preserve the specific type across the function instead of erasing it C. Generics are required by the TypeScript compiler D. There is no difference

B. Generics preserve the specific type across the function instead of erasing it — A generic keeps the relationship between input and output types specific to each call, while any discards type information entirely.

[predict-output] Given function getId<T extends { id: number }>(item: T): number, what happens when calling getId({ name: "Ada" })? A. Runs fine and returns undefined B. Compile-time error: missing required property 'id' C. Runtime error only D. TypeScript adds a default id of 0

B. Compile-time error: missing required property 'id' — The constraint extends { id: number } requires an id property; an object without one fails to satisfy the constraint at compile time.

[conceptual] What does the constraint K extends keyof T achieve in function getProp<T, K extends keyof T>(obj: T, key: K): T[K]? A. It makes K a numeric type B. It restricts K to only the actual property names of T, so invalid keys are rejected at compile time C. It has no effect — keyof is purely documentation D. It forces T to be an array type

B. It restricts K to only the actual property names of T, so invalid keys are rejected at compile time — keyof T produces a union of T's literal property names; constraining K to that union means only real keys of the passed object type-check, and the return type T[K] is exactly that property's type.

Write a generic function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] that extracts one property from every object in an array. Call it on an array of { id: number; name: string } objects to pull out just the names, and confirm the result type is string[].

function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] { return items.map(item => item[key]); } const users = [ { id: 1, name: "Ada" }, { id: 2, name: "Grace" } ]; const names = pluck(users, "name"); // string[] — ["Ada", "Grace"]

Use .map(item => item[key]) inside the function body — the K extends keyof T constraint is what makes item[key] type-check safely.