TypeScript Generics for Everyday Code
You're writing a function that returns the first element of an array. You want it to work for numbers, strings, and objects, but you also want the return type to match the input type. Without generics, you'd resort to any and lose all type safety. That's where TypeScript generics come in.
Generics let you write code that works with multiple types while preserving the specific type information. In this guide, we'll explore practical patterns for using generics in everyday TypeScript code.
What Are Generics?
Generics are a way to parameterize types. Just like functions take parameters, generics take type parameters. They allow you to create reusable components that work over a variety of types rather than a single one.
Consider this identity function:
function identity<T>(arg: T): T {
return arg;
}
Here, T is a type variable. When you call identity(42), TypeScript infers T as number. When you call identity("hello"), T becomes string. The return type always matches the input type.
Generic Functions in Practice
Let's look at a common scenario: a function that wraps a value in an object.
function wrapInObject<T>(value: T): { value: T } {
return { value };
}
Without generics, you'd have to use any or unknown, and callers would lose type information. With generics, the return type is precisely { value: T }.
Another everyday example is a function that returns the last element of an array:
function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
TypeScript infers T from the array argument, so last([1, 2, 3]) returns number | undefined.
Generic Constraints
Sometimes you want to restrict the types that can be used with your generic. For example, you might need to access a property that only exists on certain types. You can use the extends keyword to add a constraint.
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
Now T must have a length property of type number. This works for strings, arrays, and any object with a length property.
You can also use constraints with interfaces:
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
This ensures that the items passed to findById have an id property.
Generic Interfaces and Type Aliases
Generics aren't limited to functions. You can define generic interfaces and type aliases to create flexible data structures.
interface ApiResponse<T> {
data: T;
status: number;
error?: string;
}
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
const data = await response.json();
return { data, status: response.status };
}
Now you can type API responses precisely:
interface User {
id: number;
name: string;
}
const userResponse = await fetchData<User>('/api/user');
// userResponse.data is User
Generic Classes
Classes can also be generic. A common example is a simple stack:
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];
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
const num = numberStack.pop(); // number | undefined
Generics in React Components
If you're building React components, generics can make your components more reusable. For example, a list component that renders items of any type:
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map((item, index) => <li key={index}>{renderItem(item)}</li>)}</ul>;
}
Now you can use List with any type and get full type checking on the renderItem callback.
Common Pitfalls and Best Practices
- Don't overuse generics. If a function only works with a specific type, just use that type. Generics add complexity.
- Use descriptive type parameter names. While
Tis common, for multiple parameters use names likeTInput,TOutput. - Prefer constraints over
any. Constraints give you type safety while still being flexible. - Remember that generics are erased at runtime. They exist only at compile time.
Comparison: Generics vs. any vs. Union Types
| Approach | Type Safety | Flexibility | Use Case |
|---|---|---|---|
any |
None | High | Migrating JS, quick prototypes |
| Union types | Good | Limited to known types | When you know all possible types |
| Generics | Strong | High | Reusable components with preserved types |
FAQ
When should I use generics instead of any?
Use generics whenever you want to preserve type information across a function or component. any disables type checking, while generics keep it. If you find yourself reaching for any to make something work, consider if a generic could solve it more safely.
Can I use multiple type parameters?
Yes. You can define functions with multiple type parameters, like function pair<K, V>(key: K, value: V): [K, V]. Use descriptive names when there are more than one.
How do generics work with arrow functions in .tsx files?
In .tsx files, <T> can be ambiguous with JSX. Use a trailing comma: const identity = <T,>(arg: T): T => arg; or use extends unknown: <T extends unknown>.
Generics are a powerful feature that can make your TypeScript code more reusable and type-safe. Start with simple generic functions, then gradually apply them to interfaces and classes. If you need to format or validate JSON data with type safety, check out our JSON Formatter tool to quickly inspect and pretty-print your JSON.