TypeScript Generics for Beginners
The "any" Trap
When developers first transition from JavaScript to TypeScript, they usually love it—until they encounter a function that needs to handle multiple data types. At that point, frustration sets in, and they inevitably type any to silence the compiler errors.
function getFirstItem(arr: any[]): any {
return arr[0];
}
const num = getFirstItem([1, 2, 3]); // num is 'any'
const str = getFirstItem(["a", "b", "c"]); // str is 'any'
While this code runs, you have completely defeated the purpose of TypeScript. The compiler no longer knows if num is a number or if str is a string, meaning you lose all autocompletion and type safety.
This is exactly the problem that Generics were invented to solve.
What are Generics?
A Generic is simply a Type Variable. Just like a normal variable holds a value (like let x = 5), a Type Variable holds a Type (like T = number).
Instead of hardcoding a specific type, you allow the developer calling the function to pass the type in dynamically at runtime.
Here is the exact same function rewritten using a Generic:
// The <T> declares a type variable named 'T'
function getFirstItem<T>(arr: T[]): T {
return arr[0];
}
// Now TypeScript is incredibly smart!
const num = getFirstItem<number>([1, 2, 3]); // num is explicitly 'number'
const str = getFirstItem<string>(["a", "b", "c"]); // str is explicitly 'string'
Type Inference (The Magic Part)
Writing <number> or <string> every time you call a function is tedious. Fortunately, TypeScript is incredibly smart. It looks at the argument you passed in, figures out the type, and automatically sets the Generic variable for you.
// We didn't pass <number>, but TS infers that T = number because we passed an array of numbers!
const autoNum = getFirstItem([1, 2, 3]);
Real-World Use Case: API Responses
The most common and powerful use case for Generics in modern web development is handling API responses.
Imagine you have a fetchData function. Sometimes it fetches Users, sometimes it fetches Products. You want the function to return a strongly typed response depending on what you asked for.
First, define your interfaces:
interface User {
id: string;
name: string;
}
interface Product {
id: string;
price: number;
}
Next, write a Generic fetch function. Notice how we pass <T> to the native Promise object!
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
const data = await response.json();
return data as T;
}
Finally, call the function and tell it what type to expect:
// TypeScript knows 'user' has an 'id' and 'name'
const user = await fetchData<User>('/api/users/1');
// TypeScript knows 'product' has an 'id' and 'price'
const product = await fetchData<Product>('/api/products/1');
Conclusion
Generics look intimidating because of the <T> syntax, but they are conceptually very simple: they are just variables for types. By mastering Generics, you can write highly reusable, completely type-safe utility functions and API handlers, completely eliminating the need for any in your codebase.