← all articles
// article

TypeScript Generics You'll Actually Use

2025-12-06

TypeScript Generics: Not Just for Type Theory PhDs

Generics in TypeScript are tools for writing flexible, reusable code that maintains type safety. Simply put, they allow you to write a function or a component that can work with any data type, without losing information about what that type actually is. It means fewer bugs, clearer intentions, and a codebase that’s easier to maintain and extend.

Why Bother with Generics? The Case Against 'Any'

Imagine you’re building an application and need a function to fetch data from various APIs. Without generics, you might default to any, the TypeScript escape hatch:

function fetchData(url: string): Promise<any> {
  return fetch(url).then(res => res.json());
}

// Usage:
fetchData('/api/users').then(data => {
  // data is 'any' here. You have no type safety.
  // data.id, data.name, data.email -- TypeScript won't check if these exist.
});

This works, but it completely undermines TypeScript's core benefit: type checking. You’re back to JavaScript’s wild west, where a typo like data.nam would only show up at runtime, probably in production, probably at 3 AM. For a startup founder or freelancer, a bug like that means lost time, potential reputational damage, or worse, lost revenue. For a small team, chasing such errors is a drain on resources that could be spent building new features.

Generics solve this by allowing you to define a placeholder type that gets determined when the function is called. It’s like a customizable cookie cutter: you define the shape once, but you can use it with different doughs (types) to get type-safe cookies (values).

function fetchData<T>(url: string): Promise<T> {
  return fetch(url).then(res => res.json());
}

// Usage:
interface User { id: number; name: string; email: string; }
fetchData<User>('/api/users').then(users => {
  // users is now Promise<User[]> (or just User[] if not explicitly wrapped)
  // TypeScript knows 'users' is an array of User objects.
  users.forEach(user => console.log(user.name)); // 'user.name' is type-safe.
  // users.forEach(user => console.log(user.nam)); // ERROR: Property 'nam' does not exist on type 'User'.
});

This simple change ensures that when you fetch data from, say, a Stripe API endpoint for customer details, or a Vercel function returning build logs, your application's understanding of that data is precise from the moment it hits your codebase. No more guessing, no more runtime surprises.

The Classic Use Case: Reusable Utility Functions

Generics shine brightest in utility functions that need to operate on various types without knowing them upfront. Beyond fetching, consider common array manipulations or object transformations.

Identity Function (The Hello World of Generics)

A function that simply returns whatever you pass into it:

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

let output1 = identity<string>('myString'); // Type of output1 is string
let output2 = identity<number>(123);    // Type of output2 is number
let output3 = identity(true);          // Type inference works: output3 is boolean

While trivial, it clearly shows the type T is preserved. This principle extends to more complex scenarios.

Generic Container / Wrapper

Perhaps you want a function that wraps any value in a standardized object, maybe for logging or state management:

interface Container<T> {
  value: T;
  timestamp: Date;
}

function createContainer<T>(item: T): Container<T> {
  return {
    value: item,
    timestamp: new Date()
  };
}

let userContainer = createContainer({ id: 1, name: 'Alice' });
// Type of userContainer is Container<{ id: number; name: string; }>
console.log(userContainer.value.name); // Type-safe access

This pattern is remarkably useful for standardizing how data flows through your application, especially when dealing with various API responses or user inputs. It brings consistency and type safety, reducing the mental overhead for developers and the potential for bugs.

Generics in Components: The UI Advantage

For front-end development, especially with frameworks like React or Vue, generics are a superpower for building truly reusable UI components.

Type-Safe Tables or Lists

Consider a generic Table component. It needs to render a list of items, but the structure of those items (users, products, orders) will vary.

// Simplified React example
interface TableProps<T> {
  data: T[];
  renderRow: (item: T) => React.ReactNode; // A function to render each row
}

function Table<T>({ data, renderRow }: TableProps<T>) {
  return (
    <table>
      <tbody>
        {data.map((item, index) => <tr key={index}>{renderRow(item)}</tr>)}
      </tbody>
    </table>
  );
}

interface Product { id: string; name: string; price: number; }
const products: Product[] = [
  { id: '1', name: 'Laptop', price: 1200 },
  { id: '2', name: 'Mouse', price: 25 }
];

const ProductTable = () => (
  <Table<Product>
    data={products}
    renderRow={(product) => (
      <>
        <td>{product.name}</td>  <!-- 'product.name' is type-safe -->
        <td>${product.price}</td>
      </>
    )}
  />
);

This Table component doesn't care if it's displaying Products, Users, or Orders. The T placeholder makes it adaptable. When you pass Product[] as data, TypeScript automatically infers that renderRow will receive a Product object, providing full type safety inside your rendering logic. This drastically cuts down on boilerplate and ensures consistency across your UI, which is crucial for maintaining a complex application without accumulating technical debt.

Type-Safe Data Structures: Beyond the Basics

You're already using generics with built-in JavaScript types. Remember Array<string> or Promise<number>? These are generic types. You can create your own for custom data structures.

A Simple Stack

Let's build a basic Stack (Last-In, First-Out) data structure that is type-safe:

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];
  }

  isEmpty(): boolean {
    return this.items.length === 0;
  }
}

const numberStack = new Stack<number>();
numberStack.push(10);
numberStack.push(20);
// numberStack.push('hello'); // ERROR: Argument of type 'string' is not assignable to parameter of type 'number'.

const poppedNumber = numberStack.pop(); // Type of poppedNumber is 'number | undefined'

Here, Stack<T> ensures that a Stack instantiated with number can only contain numbers, guaranteeing type integrity throughout its operations. This kind of robust data structure is vital when dealing with complex application state or algorithms, especially in critical paths like payment processing (e.g., handling Stripe webhook events) or critical analytics (e.g., buffering PostHog events).

When Do Generics Become Overkill?

While powerful, generics aren't a panacea. Like any tool, they can be misused. If a function or component is truly specific to one type and will never need to operate on others, adding generics adds unnecessary cognitive load without providing any benefit. Don't force them. Sometimes, a concrete type is simply the correct choice.

Overly complex generic constraints (e.g., <T extends { id: string } & (HasName | HasTitle)>) can also make code harder to read and understand than the problem they solve. Aim for clarity and simplicity first. If generics enhance that, use them. If they obscure it, reconsider.

How SISL Approaches Generics

At SISL, a boutique web studio, we constantly strive for efficiency and maintainability in our client projects. Generics are an indispensable part of that strategy. We leverage them extensively when building custom dashboards, internal tools, and API integrations.

This pragmatic approach means we deliver robust solutions faster, with fewer bugs, and a codebase that future developers (or even the client's internal team) can understand and extend without constant hand-holding. It’s about building software that lasts, not just software that works for a week.

Your Next Step? Embrace the Power of Flexibility

Generics might seem like an advanced TypeScript topic, but their practical applications are broad and immediately beneficial. By understanding and applying these patterns, you can write code that is more robust, more reusable, and ultimately, more enjoyable to work with. It's about making your codebase a reliable asset, not a source of constant headaches.

If your project demands a codebase built for clarity, resilience, and long-term maintainability, don't hesitate to get in touch. At SISL, we specialize in crafting solutions that stand the test of time, leveraging best practices like TypeScript generics to give your business a solid digital foundation.

Got a similar problem?

Boutique web development studio from Poland — sites, WooCommerce / Magento stores, custom web apps and landings. See what we shipped.

See SISL portfolio →

Free technical audit of your site — in 24h

Core Web Vitals measured on real users, indexability, structured data, meta and internal linking. A written report with prioritised fixes, not a PDF from a generic tool. No cost, no call required.

Get the free audit →