Quick take: TypeScript generics let a function, component, or hook work with any type while preserving full type safety. Use
<T>to parameterise types,extendsto constrain them, and mapped or conditional types for advanced transformations. The most common patterns in React are generic components, typed custom hooks, and API response wrappers.
TypeScript generics let a function, component, or hook work with multiple types while preserving full type safety. The core syntax is <T>, a type parameter the TypeScript compiler infers from the call site. Use T extends SomeType to add type constraints and control which types are accepted. Generics are the right tool when the output type depends on the input type; use union types when a value can be one of a fixed set. The three most common patterns in React codebases: generic components (typed list or table props), typed custom hooks (useFetch<T> returning the right shape), and API wrappers (fetchApi<User>('/api/users/1') returning ApiResponse<User>). Avoid a generic when the type parameter appears only once, prefer unknown instead. TypeScript 5.4 added NoInfer<T> to control inference direction. TypeScript 5.5 infers type predicates in .filter() callbacks automatically, removing manual (u): u is Type annotations.
Generics are one of TypeScript's most powerful features, yet many developers avoid them or don't use them correctly. Here we'll show you practical patterns that actually appear in production React codebases. In our experience migrating large React apps to strict TypeScript, generics surface most often in three places: custom hooks, API utilities, and component prop types. Generic utility types like Partial<T>, Pick<T, K>, and ReturnType<F> are all built on generics under the hood, so understanding the basics unlocks a lot of built-in power. Once you've seen how they work in practice, you won't want to go back.
Why Do TypeScript Generics Matter?
Generics are how you build genuinely reusable components, one function or one React component that handles a string, number, or object type without duplicating the implementation. The syntax uses angle brackets (<T>) to declare a type parameter that the caller fills in at the call site.
Without generics, you're forced to choose between losing type safety (using any) or duplicating code for each type. Per the TypeScript Handbook, generics solve both problems - so you don't have to choose.
I learned this the expensive way on a client project in 2023. The codebase had 47 copies of a first(arr) helper, one for each entity type, because the original author had been told generics were "too advanced". The refactor to a single generic took two hours and reduced the codebase by 1,200 lines. The same week, I caught three real bugs that the duplicated copies had drifted apart.
// Without generics - loses type information
function first(arr: any[]): any {
return arr[0];
}
const result = first([1, 2, 3]); // type: any (all type info lost)
// With generics - preserves type information
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const result = first([1, 2, 3]); // OK: number
const name = first(['Alice', 'Bob']); // OK: string
Generics vs Union Types vs any: When to Use Each
This is the decision people actually get wrong. Here's a practical breakdown:
| Approach | Use when | Type safety | Reusability |
|---|---|---|---|
any | Migrating legacy JS, absolute last resort | None, disables type checking | High but defeats the purpose |
unknown | Value type is unknown at runtime | Forces narrowing before use | Medium |
| Union types | Fixed set of possible types (e.g. 'asc' | 'desc') | High for closed sets | Low, not reusable across types |
| Generics | Output type mirrors input type | Full, TypeScript infers everything | High, one definition covers all types |
The rule of thumb: if you know the possible values, use a union. If the caller determines the type, use a generic.
How Do Generic Components Work in React?
What's the most common React use case for generics? Typed component props:
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// Usage - TypeScript infers T from the items array
<List
items={users}
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => user.id}
/>
How Do You Type Custom Hooks with Generics?
Generic hooks are extremely useful for data fetching, form state, and local storage. Here's a useLocalStorage hook that wouldn't be type-safe without generics:
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T | ((prev: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue] as const;
}
// Usage
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');
How Do You Constrain Generic Types with extends?
Type constraints narrow what types a generic will accept. Use extends to set them:
// Only accept objects with an id property
function getById<T extends { id: string }>(items: T[], id: string): T | undefined {
return items.find((item) => item.id === id);
}
// Only accept keys that exist on the object
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Alice', age: 30, role: 'admin' };
const name = pluck(user, 'name'); // OK: string
const age = pluck(user, 'age'); // OK: number
// pluck(user, 'email') // ERROR: 'email' not a key
How Do You Type API Responses with Generics?
Here's a common pattern for typed API utilities:
interface ApiResponse<T> {
data: T;
error: string | null;
loading: boolean;
}
async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json() as T;
return { data, error: null, loading: false };
} catch (err) {
return { data: null as T, error: String(err), loading: false };
}
}
// Usage
interface User { id: string; name: string; email: string; }
const { data, error } = await fetchApi<User>('/api/users/1');
// data is typed as User
What Are Conditional Types in TypeScript?
Conditional types let you express type relationships that depend on other types. They follow the pattern T extends U ? X : Y. If T is assignable to U, the result is X, otherwise Y. They're not as scary as they look:
type NonNullable<T> = T extends null | undefined ? never : T;
type Flatten<T> = T extends Array<infer U> ? U : T;
type A = Flatten<string[]>; // string
type B = Flatten<number>; // number (not an array, returns as-is)
// Practical example: extract promise return type
type Awaited<T> = T extends Promise<infer U> ? U : T;
type UserData = Awaited<ReturnType<typeof fetchUser>>; // the resolved User type
What Are Mapped Types and When Should You Use Them?
Mapped types transform every property of a type, and they're easier than you'd think. TypeScript ships several built-in generic utility types like Partial, Pick, and Omit, and all of them use mapped types internally:
// Make all properties optional
type Partial<T> = { [K in keyof T]?: T[K] };
// Make all properties readonly
type Readonly<T> = { readonly [K in keyof T]: T[K] };
// Pick only certain keys
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
// Practical: form state from an entity type
type FormState<T> = {
[K in keyof T]: {
value: T[K];
error: string | null;
touched: boolean;
};
};
interface LoginForm { email: string; password: string; }
type LoginFormState = FormState<LoginForm>;
// { email: { value: string; error: string | null; touched: boolean }, ... }
What Are the Most Common Generic Mistakes?
Don't overuse generics. If a type parameter appears only once and doesn't connect input to output, you're better off using a concrete type or unknown instead:
// BAD: Unnecessary generic
function log<T>(value: T): void {
console.log(value);
}
// GOOD: Just use unknown (or any if you need it)
function log(value: unknown): void {
console.log(value);
}
Do use generics when the return type mirrors the input:
// GOOD: The output type depends on the input type
function identity<T>(value: T): T {
return value;
}
Think of generics as "type variables" - they're regular variables, but for types instead of values.
What Changed for Generics in TypeScript 5.4, 5.5, and 7?
Generic syntax itself hasn't changed across these versions, <T extends SomeType> works the same as always. What did improve is inference and tooling performance.
TypeScript 5.4: NoInfer<T>
A utility type that prevents TypeScript from using a parameter for type inference. It's useful when you want one arg to set the type and another to validate against it:
function createPalette<T extends string>(colors: T[], defaultColor: NoInfer<T>): Record<T, string> {
// defaultColor won't widen the T inference, it must match what colors already determined
const palette = {} as Record<T, string>;
return palette;
}
// T is inferred from colors as 'red' | 'blue'
// defaultColor must be exactly 'red' | 'blue', not a different string
createPalette(['red', 'blue'], 'red'); // OK
createPalette(['red', 'blue'], 'green'); // Error: 'green' not assignable to 'red' | 'blue'
Before NoInfer<T>, you'd need a second generic parameter and a constraint. Now it's one line.
TypeScript 5.5: Inferred Type Predicates
TypeScript 5.5 infers type guard predicates automatically, no more manual value is Type annotations for simple patterns:
const users = [null, { name: 'Alice' }, null, { name: 'Bob' }];
// Before TS 5.5, you needed an explicit type predicate
const filtered = users.filter((u): u is { name: string } => u !== null);
// TS 5.5+, TypeScript infers the type predicate automatically
const filtered = users.filter(u => u !== null); // type: { name: string }[]
Your typed filter helpers no longer need manual annotations, great news for generic utility functions.
TypeScript 7 (Corsa): Compilation Speed
TypeScript 7's native Go compiler doesn't touch generic syntax, but it handles deeply nested generics (common in API wrappers and React form libraries) 10x faster than the JS compiler. The TypeScript compiler processes the same type inference logic; it's just running as a native binary now, shipped as the standard tsc since the July 2026 GA. Want the full story on the rewrite? Start with the TypeScript 7 overview. If CI type-checks take 30+ seconds on complex generics, the TS 7 migration is worth it.
Related
- TypeScript Utility Types: A Practical Developer Guide - Partial, Pick, Omit, and Record are all built on generics; this guide covers when to use each one
- React 19 guide - React 19's Actions pattern benefits from typed generics for
useActionStateanduseOptimistic - React Hook Form - typed form validation using generic
FieldValuesandUseFormReturn<T>patterns - TanStack Query - all
useQueryanduseMutationhooks are fully generic; typed API responses out of the box - TypeScript 7 Migration Guide - migrate to tsgo for 10x faster compilation of generic-heavy codebases
- TypeScript types for AI - typed interfaces constrain what AI code generators can produce, and generics make those interfaces reusable