The Problem: The Need for Type-safe, Reusable Code

Imagine you need a function that takes an argument and returns it. Without generics, you might write it like this:

TypeScript


// The 'any' approach (loses type information)
function identity(arg: any): any {
  return arg;
}

const output = identity("hello"); // 'output' is of type 'any', we don't know it's a string.

This works, but we lose the type information. We don't know that if we pass in a string, we get a string back.

Hello, Generics!

Generics solve this by allowing you to create components that can work over a variety of types rather than a single one. We use a type variable, commonly written as <T>, to stand in for the actual type.

TypeScript


// The Generic approach (preserves type information)
function identity<T>(arg: T): T {
  return arg;
}

// The type 'T' is captured here.
const outputString = identity<string>("hello"); // 'outputString' is now known to be a 'string'.
const outputNumber = identity<number>(42);     // 'outputNumber' is now a 'number'.

// Type inference also works! TS can figure out the type for us.
const inferredOutput = identity("world"); // 'inferredOutput' is inferred as 'string'.

By using <T>, we've created a link between the input type and the output type. The function is now type-safe and reusable.

You can also use generics with interfaces and classes:

TypeScript


interface GenericBox<T> {
  contents: T;
}

let boxOfStrings: GenericBox<string> = { contents: "my secret" };
let boxOfNumbers: GenericBox<number> = { contents: 123 };
Generic Constraints

Sometimes you need to ensure your generic type has certain properties. You can do this by using the extends keyword.

TypeScript


interface HasLength {
  length: number;
}

// This generic function will only accept types that have a .length property.
function logLength<T extends HasLength>(arg: T): void {
  console.log(arg.length);
}

logLength("hello"); // Works (string has .length)
logLength([1, 2, 3]); // Works (array has .length)
// logLength(42); // ERROR: Argument of type 'number' is not assignable to 'HasLength'.
Built-in Utility Types

TypeScript comes with a set of powerful utility types that perform transformations on existing types. They are generics themselves! Here are a few of the most common ones:

Partial<T> Makes all properties of type T optional.

TypeScript


interface Todo {
  title: string;
  description: string;
}

// 'Partial<Todo>' is equivalent to: { title?: string; description?: string; }
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return { ...todo, ...fieldsToUpdate };
}

Readonly<T> Makes all properties of type T read-only.

TypeScript


const todo: Readonly<Todo> = {
  title: "Do laundry",
  description: "Don't forget the fabric softener!"
};
// todo.title = "New Title"; // ERROR: Cannot assign to 'title' because it is a read-only property.

Pick<T, K> Creates a new type by picking a set of properties K from type T.

TypeScript


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

// 'UserPreview' will be { name: string; email: string; }
type UserPreview = Pick<User, "name" | "email">;

const userDisplay: UserPreview = { name: "Alice", email: "alice@example.com" };

Omit<T, K> The opposite of Pick. Creates a type by removing a set of properties K from T.

TypeScript


// 'UserForUpdate' will be { name: string; email: string; age: number; }
type UserForUpdate = Omit<User, "id">;