Tools Games AI
[ Ad Placement: Top Article Banner ]

TypeScript Generics: Writing Reusable Code

The Problem with `any`

TypeScript is amazing because it catches errors before your code even runs. But developers frequently hit a wall when writing functions that need to accept multiple data types. If you write a function that reverses an array, what type should the array be? If you define it as number[], it won't work for strings. If you define it as any[], you have defeated the entire purpose of TypeScript, completely losing autocomplete and type safety on the result.

Enter Generics: Type Variables

Generics allow you to write functions, classes, and interfaces that take Types as arguments, just like regular functions take values as arguments. You define a generic type parameter (usually denoted by a capital letter like <T>) and use it throughout your function.

// The T captures whatever type the user passes in
function reverseArray<T>(items: T[]): T[] {
  return items.reverse();
}

// TypeScript knows 'numbers' is specifically an array of numbers
const numbers = reverseArray<number>([1, 2, 3]); 

// TypeScript knows 'words' is specifically an array of strings
const words = reverseArray<string>(["hello", "world"]); 

You don't even always need to write the <string> explicitly. TypeScript is incredibly smart with "Type Inference" and can usually guess what T is based on the data you pass in.

Real World Application: API Responses

Generics are absolutely mandatory when dealing with API responses. In a large enterprise application, you will have dozens of different endpoints returning different data objects (Users, Products, Invoices). However, the overall structure of an API response is usually the same: it contains a status code, an error message, and a data payload.

Instead of writing 50 different interfaces, you write one Generic Interface:

interface ApiResponse<T> {
  status: number;
  message: string;
  data: T; // The shape of 'data' changes based on what we pass in
}

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

// Using the Generic
async function fetchUser(): Promise<ApiResponse<User>> {
  const res = await fetch('/api/user');
  return res.json();
}

fetchUser().then(response => {
  // TypeScript provides perfect autocomplete here!
  // It knows response.data has a .name property.
  console.log(response.data.name); 
});

Generic Constraints

Sometimes you want a Generic to be flexible, but not too flexible. You can use the extends keyword to force the generic type to have certain properties. For example, if you write a function that logs the length of an object, you can ensure that T must at least have a .length property:

interface HasLength { length: number; }

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

logLength([1, 2, 3]); // Works, arrays have .length
logLength("Hello");   // Works, strings have .length
// logLength(42);     // ERROR: Numbers do not have a .length property!
[ Ad Placement: Bottom Article Banner ]