TypeScript Generics for Everyday Code: A Practical Guide

Frontend2026-10-04TryQuickToolBox

You're writing a function that works with any type, but TypeScript keeps complaining about any. Or you've copied the same function three times just to handle different types. Generics solve this—they let you write code that works with multiple types while preserving type safety. But many developers either avoid them or overuse them. This guide shows you how to use generics in everyday code without the headache.

What Are Generics and Why Should You Care?

Generics are a way to parameterize types. Instead of hardcoding a type like string or number, you use a type variable (often T) that gets replaced with a concrete type when the function or class is used.

Why does this matter? Because it lets you write reusable code that stays type-safe. Without generics, you'd either duplicate code or use any, which defeats the purpose of TypeScript.

Generic Functions: The Building Block

Let's start with a simple example. Suppose you want a function that returns the first element of an array.

function firstElement(arr: any[]): any {
  return arr[0];
}

This works, but you lose type information. If you pass an array of strings, the return type is any, not string. Now with generics:

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

Now TypeScript knows that if you pass a string[], you get a string | undefined back. The type is preserved.

You can also use multiple type parameters:

function pair<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}

Generic Interfaces and Type Aliases

Generics aren't limited to functions. You can create generic interfaces and type aliases to describe flexible data structures.

interface Box<T> {
  value: T;
}

const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };

This is especially useful for API responses. Instead of defining a new interface for every endpoint, you can define a generic ApiResponse<T>:

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

type User = { id: number; name: string };

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

Generic Constraints: Adding Rules

Sometimes you want to restrict what types can be used with your generic. That's where constraints come in. Use the extends keyword to require that the type has certain properties.

function getLength<T extends { length: number }>(item: T): number {
  return item.length;
}

getLength("hello"); // OK
getLength([1, 2, 3]); // OK
getLength(42); // Error: number doesn't have length

Constraints make your generics more predictable and prevent runtime errors.

Generic Classes

Classes can also be generic. This is handy for data structures like stacks, queues, or repositories.

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 top = numberStack.pop(); // number | undefined

When to Use Generics (and When Not To)

Generics are powerful, but they can make code harder to read if overused. Here's a quick guide:

Use Generics When Avoid Generics When
You need to preserve type relationships A simple union type or any is enough
You're writing reusable utilities The type is always the same
You want to avoid code duplication It makes the code harder to understand

A good rule of thumb: if you find yourself writing the same logic for multiple types, consider generics. If you're adding a third type parameter and nobody understands it, step back.

Common Patterns and Best Practices

Real-World Example: A Generic API Client

Let's tie it together with a practical example: a simple API client that fetches data and returns a typed response.

async function fetchData<T>(url: string): Promise<T> {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  return response.json() as Promise<T>;
}

interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}

async function main() {
  const post = await fetchData<Post>("https://jsonplaceholder.typicode.com/posts/1");
  console.log(post.title); // string
}

This pattern is common in frontend code. You get type safety without repeating yourself for every endpoint.

FAQ

What does the <T> syntax mean in TypeScript?

<T> declares a type parameter. It's a placeholder for a type that will be specified when the function, interface, or class is used. You can name it anything, but T is conventional for a single type.

Can I use multiple generic type parameters?

Yes. You can define as many as you need, separated by commas: <T, U, V>. Use descriptive names to keep your code readable.

How do I constrain a generic to only accept objects?

Use T extends object. This ensures that T is an object type and not a primitive like string or number. You can also constrain to specific shapes, like T extends { id: number }.

Generics are a core part of writing maintainable TypeScript. If you're working with JSON data and want to quickly validate or format it, try our JSON Formatter to pretty-print and inspect your API responses.