Generics in TypeScript

· 4 min read · Updated March 7, 2026 · beginner
generics types intermediate

Generics are one of TypeScript’s most powerful features. They let you write flexible, reusable code while maintaining full type safety. Instead of locking your functions or classes to a single type, generics allow you to parameterize types — just like functions parameterize values.

Why Generics Matter

Imagine you need a function that returns whatever you pass into it:

function identity(arg: any): any {
  return arg;
}

This works, but you’ve lost type information. When you call identity("hello"), TypeScript has no idea it returns a string. Generics solve this:

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

const str = identity("hello");  // type: string
const num = identity(42);         // type: number

The <T> is a type parameter — a placeholder that gets filled in when you call the function. TypeScript infers T from the argument, so you get full type safety without extra code.

Generic Functions

Generic functions use one or more type parameters:

function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

const nums = [1, 2, 3];
const firstNum = firstElement(nums);  // type: number

const words = ["hello", "world"];
const firstWord = firstElement(words);  // type: string

You can also explicitly specify type arguments:

const explicit = firstElement<string>(["hello"]);  // type: string

Generic Interfaces and Types

Generics work with interfaces too:

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

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

const userResponse: ApiResponse<User> = {
  data: { id: 1, name: "Alice" },
  status: 200,
  message: "Success"
};

The ApiResponse<T> interface adapts to whatever data type you need — whether that’s a User, an array, or anything else.

Generic Constraints

Sometimes you need to restrict what types can be used. Use extends to add constraints:

interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(item: T): void {
  console.log(item.length);
}

logLength("hello");      // OK - strings have length
logLength([1, 2, 3]);    // OK - arrays have length
logLength({ length: 5 }); // OK - object has length property
// logLength(42);        // Error - numbers don't have length

You can also constrain one generic type parameter based on another:

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

const user = { name: "Bob", age: 30 };
const name = getProperty(user, "name");  // type: string
const age = getProperty(user, "age");    // type: number
// getProperty(user, "email");           // Error - 'email' not in user

Generic Classes

Classes can be generic too:

class Box<T> {
  contents: T;
  
  constructor(contents: T) {
    this.contents = contents;
  }
  
  get(): T {
    return this.contents;
  }
}

const stringBox = new Box("books");
const numBox = new Box(123);

You can even constrain generic class types:

class NamedBox<T extends { name: string }> {
  item: T;
  
  constructor(item: T) {
    this.item = item;
  }
  
  introduce(): string {
    return `This is ${this.item.name}`;
  }
}

const box = new NamedBox({ name: "Widget", price: 10 });
console.log(box.introduce());  // This is Widget

Multiple Type Parameters

Functions can accept multiple type parameters:

function pair<K, V>(key: K, value: V): [K, V] {
  return [key, value];
}

const idName = pair(1, "Alice");      // [number, string]
const boolStr = pair(true, "enabled"); // [boolean, string]

This is useful for maps, caches, and other data structures:

function createCache<K, V>(): Map<K, V> {
  return new Map();
}

const userCache = createCache<string, User>();
userCache.set("alice", { id: 1, name: "Alice" });

Default Type Parameters

You can provide default types for generics:

interface ApiResponse<T = unknown> {
  data: T;
  status: number;
}

const simpleResponse: ApiResponse = {
  data: "ok",
  status: 200
};

Here, T defaults to unknown when not specified.

Practical Example: Generic Repository

Here’s how generics shine in real-world code:

interface Repository<T> {
  findById(id: string): Promise<T | null>;
  findAll(): Promise<T[]>;
  save(item: T): Promise<T>;
  delete(id: string): Promise<void>;
}

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

class UserRepository implements Repository<User> {
  private users: Map<string, User> = new Map();
  
  async findById(id: string): Promise<User | null> {
    return this.users.get(id) || null;
  }
  
  async findAll(): Promise<User[]> {
    return Array.from(this.users.values());
  }
  
  async save(user: User): Promise<User> {
    this.users.set(user.id, user);
    return user;
  }
  
  async delete(id: string): Promise<void> {
    this.users.delete(id);
  }
}

The Repository<T> interface works for any entity — User, Product, Order — while maintaining full type safety throughout.

Summary

Generics let you write code that’s both flexible and type-safe:

  • Type parameters (<T>) make functions and classes reusable
  • Type inference means you often don’t need to specify types explicitly
  • Constraints (extends) restrict what types are allowed
  • Default types provide fallback when types aren’t specified

Master generics, and you’ll write TypeScript code that’s more maintainable, more reusable, and catches more errors at compile time.