TypeScriptIntermediate

TypeScript Generics Explained: A Practical Introduction

Generics look intimidating in library type definitions but the core idea is simple — a type that takes a type as a parameter. Here's how to actually use and read them.

DevFieldGuideJuly 16, 2026 (updated July 22, 2026)6 min read
Share:

Generics are the reason a function like Array.prototype.map can transform string[] into number[] and have TypeScript track the correct type the whole way through — without a generic, that's not possible to express accurately.

The problem generics solve

Without generics, a reusable function has to either give up on specific types or write one version per type:

ts
// Loses type information — box.contents is always `any`
function makeBox(value: any) {
  return { contents: value };
}
 
// Repeats the same logic for every type
function makeStringBox(value: string) { return { contents: value }; }
function makeNumberBox(value: number) { return { contents: value }; }

Neither is good — the first loses type safety, the second doesn't scale.

The generic version

ts
function makeBox<T>(value: T) {
  return { contents: value };
}
 
const stringBox = makeBox("hello");   // { contents: string }
const numberBox = makeBox(42);         // { contents: number }

<T> declares a type parameter — a placeholder type that gets filled in based on whatever's actually passed. TypeScript infers T from the argument automatically; you rarely need to write makeBox<string>("hello") explicitly.

Generic interfaces and types

The same pattern applies to interfaces, not just functions — useful for anything that wraps another type generically:

ts
interface ApiResponse<T> {
  data: T;
  status: number;
  error: string | null;
}
 
async function fetchUser(): Promise<ApiResponse<User>> {
  const res = await fetch("/api/user");
  return res.json();
}

ApiResponse<User> and ApiResponse<Product> share the exact same shape (data, status, error) but data is correctly typed as User or Product respectively — one interface definition instead of one per response type.

Constraining a generic with extends

Sometimes a generic function needs to guarantee the type has at least certain properties, without locking it to one exact type:

ts
interface HasId {
  id: string;
}
 
function findById<T extends HasId>(items: T[], id: string): T | undefined {
  return items.find((item) => item.id === id);
}
 
findById([{ id: "1", name: "Ada" }], "1"); // works — has an id
findById([{ name: "Ada" }], "1");           // Error: missing "id"

T extends HasId means "T can be any type, as long as it has at least an id: string property" — the function works with any object shape that satisfies that minimum, while still preserving the full original type in the result.

Multiple type parameters

Generics aren't limited to one type parameter — a function combining two different collections often needs two:

ts
function zip<A, B>(a: A[], b: B[]): [A, B][] {
  return a.map((item, i) => [item, b[i]]);
}
 
zip([1, 2, 3], ["a", "b", "c"]);
// [[1, "a"], [2, "b"], [3, "c"]] — typed as [number, string][]

Default type parameters

A generic can specify a default, used when nothing more specific is provided at the call site:

ts
interface FetchOptions<T = unknown> {
  url: string;
  parser?: (raw: string) => T;
}
 
const opts: FetchOptions = { url: "/api/data" }; // T defaults to unknown

This keeps the common, unspecified case simple while still allowing callers to specify FetchOptions<User> explicitly when they know the actual shape.

Generic classes

The same pattern applies to classes, useful for a data structure that should work with any type consistently:

ts
class Stack<T> {
  private items: T[] = [];
 
  push(item: T): void {
    this.items.push(item);
  }
 
  pop(): T | undefined {
    return this.items.pop();
  }
}
 
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
 
const stringStack = new Stack<string>();
stringStack.push("a"); // stringStack.push(1) would be a type error

One Stack implementation serves both number and string (or any other type) stacks, each fully type-checked as its own specific type — without generics, this would require either a separate class per type or falling back to any and losing type safety entirely.

keyof and generic property access

A common, genuinely useful generic pattern: a function that safely accesses a property by name, with the return type automatically inferred from which property was requested:

ts
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
 
const user = { name: "Ada", age: 30 };
getProperty(user, "name"); // typed as string
getProperty(user, "age");  // typed as number
getProperty(user, "email"); // Error: "email" is not a key of the user object

K extends keyof T constrains K to only the actual property names that exist on T — attempting to access a property that doesn't exist is caught at compile time, not discovered at runtime as undefined.

Generics in React components

The same generic pattern extends to React component props, useful for a component genuinely reusable across different data shapes:

tsx
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}
 
function List<T>({ items, renderItem }: ListProps<T>) {
  return <ul>{items.map((item, i) => <li key={i}>{renderItem(item)}</li>)}</ul>;
}
 
<List items={users} renderItem={(user) => user.name} />
<List items={products} renderItem={(product) => product.title} />

One List component serves both User[] and Product[] data, each call fully type-checked — renderItem is correctly typed as receiving a User in the first case and a Product in the second, inferred automatically from the items array passed.

Common mistakes

Common mistakes
  • Reaching for a generic when the function only ever needs to work with one concrete type. Generics add real complexity — if a function genuinely only ever handles string, a plain string parameter is more readable than an unnecessary T.
  • Forgetting extends when a generic function needs to access a specific property. Without a constraint, TypeScript only knows T is "some type" and won't let you access .id or any other property on it, even if every real caller happens to pass an object that has one.
  • Using any instead of a generic to "make an error go away." This is almost always the wrong fix — it silently disables type checking for that value everywhere it flows, rather than preserving type safety through the function the way a generic does.
  • Over-constraining a generic with a type so specific it might as well not be generic at all — if T extends ExactlyOneSpecificShape, consider whether the function actually needs a fixed, non-generic parameter type instead.

Frequently Asked Questions

DevFieldGuide
DevFieldGuide

Editorial Team

Practical tutorials and developer tools, written and maintained by the DevFieldGuide team.

Enjoyed this article?

Get the next one straight to your inbox, along with the best of what we publish each week.

Related Articles

More in TypeScript

View all