Special Sponsor:PromptBuilder— Fast, consistent prompt creation powered by 1,000+ expert templates.
Make your Product visible here.Contact Us

Home/TypeScript Generics

TypeScript Generics: Complete Guide with Examples

Generics are TypeScript's most powerful tool for writing reusable, type-safe code. This guide covers generic functions, interfaces, classes, constraints, default type parameters, and conditional types — each with real, runnable examples.

What Are TypeScript Generics?

A generic is a type that acts as a placeholder — written as T,U, or any identifier — that gets filled in when you call a function or instantiate a class. Without generics, you either write separate functions for every type (repetitive) or use any (unsafe). Generics give you the best of both.

// Without generics — forced to use 'any', loses type info
function identity(arg: any): any {
  return arg;
}
const result = identity(42); // type: any — no autocomplete, no safety

// With generics — type is preserved
function identity<T>(arg: T): T {
  return arg;
}
const num = identity(42);        // type: number
const str = identity("hello");   // type: string
const arr = identity([1, 2, 3]); // type: number[]

Generic Functions

You can add type parameters to any function. TypeScript infers them from the arguments in most cases, so you rarely need to write them explicitly.

// Returns first element — preserves element type
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}
const n = first([1, 2, 3]);   // type: number | undefined
const s = first(["a", "b"]);  // type: string | undefined

// Swap two values — both types preserved independently
function swap<A, B>(a: A, b: B): [B, A] {
  return [b, a];
}
const [x, y] = swap(1, "hello"); // x: string, y: number

// Map over array with type transformation
function mapArray<T, U>(arr: T[], fn: (item: T) => U): U[] {
  return arr.map(fn);
}
const lengths = mapArray(["cat", "elephant"], (s) => s.length);
// type: number[]

Generic Interfaces

Generic interfaces let you describe shapes that work with many types. A common pattern is an API response wrapper that holds a typed data field.

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  error?: string;
}

interface User {
  id: number;
  name: string;
  email: string;
}

// Specialise the generic interface for specific payloads
type UserResponse  = ApiResponse<User>;
type UsersResponse = ApiResponse<User[]>;
type TokenResponse = ApiResponse<{ token: string; expiresIn: number }>;

// Repository pattern — generic CRUD interface
interface Repository<T, ID> {
  findById(id: ID): Promise<T | null>;
  findAll(): Promise<T[]>;
  save(entity: T): Promise<T>;
  delete(id: ID): Promise<void>;
}

class UserRepository implements Repository<User, number> {
  async findById(id: number) { /* ... */ return null; }
  async findAll() { return []; }
  async save(user: User) { return user; }
  async delete(id: number) { /* ... */ }
}

Generic Classes

Classes can have type parameters just like functions and interfaces. The type parameter is specified when instantiating the class.

class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  get size(): number {
    return this.items.length;
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.pop(); // type: number | undefined

const stringStack = new Stack<string>();
stringStack.push("hello");
// stringStack.push(42); // Error: Argument of type 'number' is not assignable to 'string'

Generic Constraints with extends

Use extends to restrict what types are valid for T. This lets you safely access properties or methods that exist on the constraint type.

// T must have a .length property
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}
longest("alice", "bob");      // string
longest([1, 2, 3], [1, 2]);   // number[]
// longest(10, 20);            // Error: number has no .length

// K must be a key of T — prevents typos
function getField<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
const user = { name: "Alice", age: 30 };
getField(user, "name");     // type: string
getField(user, "age");      // type: number
// getField(user, "email"); // Error: not a key of typeof user

// Constrain to specific union
function wrapInArray<T extends string | number>(val: T): T[] {
  return [val];
}
wrapInArray("hello"); // string[]
wrapInArray(42);      // number[]
// wrapInArray(true); // Error: boolean is not string | number

Default Type Parameters

Like function default arguments, type parameters can have defaults. If no type argument is provided, TypeScript falls back to the default.

interface PaginatedList<T = Record<string, unknown>> {
  items: T[];
  total: number;
  page: number;
  pageSize: number;
}

// Using without type arg — defaults to Record<string, unknown>
const generic: PaginatedList = { items: [], total: 0, page: 1, pageSize: 20 };

// Using with explicit type arg
const users: PaginatedList<User> = {
  items: [{ id: 1, name: "Alice", email: "alice@example.com" }],
  total: 1,
  page: 1,
  pageSize: 20,
};

// Default can reference earlier type params
interface Pair<A, B = A> {
  first: A;
  second: B;
}
const same: Pair<number> = { first: 1, second: 2 }; // B defaults to A (number)
const diff: Pair<number, string> = { first: 1, second: "two" };

Conditional Types

Conditional types select one of two types based on a type relationship, using the ternary-like syntax T extends U ? X : Y. The infer keyword extracts sub-types within the condition.

// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>;      // false

// Unwrap a Promise to its resolved type
type Awaited<T> = T extends Promise<infer U> ? U : T;
type C = Awaited<Promise<User>>; // User
type D = Awaited<string>;        // string (not a Promise)

// Extract element type from array
type ElementOf<T> = T extends (infer U)[] ? U : never;
type E = ElementOf<string[]>; // string
type F = ElementOf<number[]>; // number

// Distributive conditional types — applied to each union member
type NonNullable<T> = T extends null | undefined ? never : T;
type G = NonNullable<string | null | undefined>; // string

// Flatten nested arrays one level
type Flatten<T> = T extends Array<infer U> ? U : T;
type H = Flatten<string[]>;  // string
type I = Flatten<string>;    // string (unchanged)

Frequently Asked Questions

What are generics in TypeScript?

Generics allow you to write reusable, type-safe code using type parameters (written as <T>) that are filled in at call-time or instantiation-time. Instead of writing separate functions for each type, you write one generic function that works correctly with any type.

What does <T extends ...> mean in TypeScript?

The 'extends' keyword in a generic constraint restricts which types can be used as T. For example, <T extends object> means T must be an object type, and <T extends keyof U> means T must be one of the property keys of U.

Can TypeScript generics have default types?

Yes. You provide a default with <T = DefaultType>. If no type argument is supplied, TypeScript uses the default. For example, interface Container<T = string> means Container defaults to Container<string>.

What is the difference between a generic function and a generic interface?

A generic function has the type parameter on the function itself — the caller decides T at each call site. A generic interface has the type parameter on the interface — the consumer decides T when creating a value of that interface type.

What are conditional types in TypeScript?

Conditional types use T extends U ? X : Y to select one of two types based on assignability. Combined with 'infer', they can extract parts of a type — for example, type Unwrap<T> = T extends Promise<infer U> ? U : T.

When should I use generics instead of 'any'?

Always prefer generics over 'any' when you need flexibility with type safety. 'any' turns off all type checking. Generics preserve the relationship between input and output types so TypeScript can catch mismatches at compile time.

Convert JavaScript to TypeScript instantly

Paste your JS code and get type-annotated TypeScript back in seconds — including generic type inference.

Convert now →

From the blog

View all →