Introduction to TypeScript Generics

Generics are often considered one of the most challenging parts of TypeScript, but they are also the most powerful. In essence, generics allow you to write code that is "type-agnostic"—it doesn't care about the specific data type it's handling right now, but it ensures that whatever type is used stays consistent throughout the execution.

By using generics, you can build components that are highly reusable while maintaining the strict type safety that makes TypeScript so valuable. Instead of using any and losing all the benefits of type checking, generics let you capture the type provided by the user and use it to enforce rules later.

Developer Tip: Think of Generics as "variables for types." Just as you pass arguments to a function, you pass types to a generic component.

 

What are Generics?

Generics act as placeholders for types. In a standard function, you define parameters for values; in a generic function, you also define parameters for the types of those values. When the code runs, TypeScript "fills in" these placeholders based on the actual data you provide.

Generics are identified by angle brackets (<T>). While T is the conventional shorthand for "Type," you can use any descriptive name, such as <UserType> or <Entity>.

Common Mistake: Using any instead of Generics. While any allows any type, it "silences" the compiler. Generics, however, "remember" the type, providing full autocompletion and error checking.

 

Why Use Generics?

  • Reusability: You can write a single logic block (like a data fetcher or a list sorter) that works for Users, Products, or Orders without rewriting the logic for each.
  • Type Safety: TypeScript tracks the specific type through your logic. If you pass a number into a generic function, TypeScript knows the return value is a number.
  • Cleaner Code: It reduces "type casting" (using as string) and prevents code duplication across your codebase.
Best Practice: Use generics whenever you find yourself writing the exact same logic for different data types. It makes your codebase much easier to maintain.

 

Syntax of Generics

To define a generic, you place a type variable inside angle brackets immediately before the function's parentheses.

function identity<T>(value: T): T {
  return value;
}
  • <T>: This declares a type parameter T. It tells TypeScript, "We're going to use a type here that we'll define later."
  • value: T: This ensures the input matches our type T.
  • : T: This guarantees the function returns the exact same type that was passed in.

 

Example of Generics

1. Generic Function

A common real-world use case for a generic function is a utility that logs a value and returns it, or a function that wraps data in a consistent format.

function logAndReturn<T>(value: T): T {
  console.log("Processing:", value);
  return value;
}

const num = logAndReturn(100);        // T is inferred as number
const str = logAndReturn("Hello");   // T is inferred as string
  • In the first call, TypeScript sees 100 and automatically "locks" T to number.
  • In the second call, T becomes string. You don't have to manually tell TypeScript the type; it's smart enough to infer it.

2. Generic with Multiple Type Parameters

Sometimes you need to handle more than one type at once. You can use multiple placeholders by separating them with commas.

function mapPair<K, V>(key: K, value: V): string {
  return `Key: ${key}, Value: ${value}`;
}

const result = mapPair(1, "Admin"); 
// K is number, V is string

This is extremely common in modern web development, such as when dealing with "Key-Value" pairs in a dictionary or state management system.

Developer Tip: While T, U, and V are standard conventions, don't be afraid to use descriptive names like <Data, Error> if it makes your code more readable for your team.

 

Generic Classes

Classes can also benefit from generics. A classic example is a State container or a Repository that handles database operations for different types of entities.

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

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

  getItems(): T[] {
    return this.items;
  }
}

const textStorage = new DataStorage<string>();
textStorage.addItem("TypeScript"); // Valid
// textStorage.addItem(500); // Error: Argument of type 'number' is not assignable to 'string'
  • The DataStorage class is now flexible. You can create a storage for strings, a storage for numbers, or even a storage for complex User objects.
Watch Out: Generic type parameters are only available on the instance side of the class, not the static side. You cannot use T in static methods or properties.

 

Generic Interfaces

Interfaces often use generics to define the shape of API responses, which usually have a standard "wrapper" but different "data" payloads.

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

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

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

This approach allows you to reuse the ApiResponse interface for every single endpoint in your application while keeping the data property type-safe.

 

Constraints on Generics

Sometimes you want a function to be generic, but only for types that have certain properties. For example, if you want to access a .length property, you must ensure the type has it. You do this using the extends keyword.

interface HasLength {
  length: number;
}

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

logLength("Hello");       // Works (strings have length)
logLength([1, 2, 3]);    // Works (arrays have length)
// logLength(123);       // Error: number does not have a length property
Best Practice: Use constraints to make your generics more predictable. It allows you to treat the generic variable as if it were a specific interface, giving you access to its methods and properties safely.

 

Generic Defaults

Just like function parameters can have default values, generic type parameters can have default types. This is useful when you want a component to be generic but usually expect it to handle one specific type.

interface Config<T = string> {
  value: T;
}

const defaultConfig: Config = { value: "Theme-Dark" }; // T defaults to string
const customConfig: Config<number> = { value: 404 };  // T is manually set to number

 

Summary

Generics in TypeScript are a cornerstone of professional-grade code. They allow you to build logic that is flexible enough to handle any data while remaining strict enough to prevent bugs before they happen. By mastering generics, you move from writing simple scripts to building scalable libraries and applications.

  • Generic Function: Allows logic to adapt to the type of the arguments passed to it.
  • Generic Classes and Interfaces: Create "blueprints" that can be customized with different data types.
  • Constraints: Use the extends keyword to limit generics to types that meet specific requirements.
  • Default Types: Simplify your code by providing a fallback type for your generics.

As you continue your TypeScript journey, look for patterns in your code where you are repeating logic for different types—that is usually the perfect place to implement a Generic.