Generic vs any.
Why preserves what any throws away.
Both any and let a function accept values of any type. The difference is what happens at the call site. With any, the return type is also any — you've lost information. With , the return type reflects what you passed in. This page shows why generics are strictly better for reusable code, and when any is legitimately the right tool.
Short answer: any says "I don't know, don't check." says "I don't know yet, but I'll track it." Use generics whenever the input and output types are related — identity, first, map, pluck, groupBy. The caller's specific type flows through the function and comes out the other side. any throws that type away. For reusable code, generics are always the right choice.
✅ Solution 1 — The identity function
The simplest case: input type = output type.
The identity function is where the difference is clearest. It takes a value and returns it unchanged. With any, the function works but the type information is destroyed at every call site. With , the type flows through — pass a string, get back a string.
// ─── Version 1: with any ──────────────────── function identityAny(x: any): any { return x; } const a = identityAny('hello'); // a: any — type lost const b = identityAny(42); // b: any const c = identityAny({ name: 'x' }); // c: any // No autocomplete, no type checking: a.toUpperCase(); // ✅ compiles ❌ would crash if a were a number b.toUpperCase(); // ✅ compiles ❌ crashes at runtime // ─── Version 2: with───────────────────── function identity(x: T): T { return x; } const s = identity('hello'); // s: string const n = identity(42); // n: number const u = identity({ name: 'x' }); // u: { name: string } // Autocomplete and checking work: s.toUpperCase(); // ✅ string method available // n.toUpperCase(); // ❌ Property 'toUpperCase' does not exist on 'number' // u.toUpperCase(); // ❌ Property 'toUpperCase' does not exist on '{ name: string }' // ─── Explicit type argument ────────────────── const explicit = identity<string>('x'); // same as inferred // ─── Why this matters ──────────────────────── // // With any: // identity('hello') → any → every downstream // operation is unchecked. Bugs compile. // // With : // identity('hello') → string → methods are // type-checked. Wrong methods are compile errors. // // One line of code. Full type preservation. console.log(a, b, c, s, n, u, explicit);
function identityAny(x) { return x; } const a = identityAny('hello'); const b = identityAny(42); const c = identityAny({ name: 'x' }); function identity(x) { return x; } const s = identity('hello'); const n = identity(42); const u = identity({ name: 'x' }); const explicit = identity('x'); console.log(a, b, c, s, n, u, explicit);
any throws away
The return type is any regardless of input. No autocomplete, no checking, no relationship between input and output.
tracks
The return type is the same T. Pass string, get string. Pass User, get User. The relationship is preserved.
Inference fills T
You rarely write identity. TypeScript infers T from the argument. Only specify explicitly when needed.
Same runtime, different safety
Both compile to identical JavaScript. The difference is entirely at compile time — the types any throws away vs
✅ Solution 2 — Real functions: first, map, pluck
Where generics genuinely matter — data transformations.
The difference is starker with real functions. A first function that returns any loses the element type. A map that returns any[] loses the transformation result. With generics, the caller's specific types flow through — pass User[], get User; pass a callback that returns number, get number[].
// ─── first: any vs────────────────────── function firstAny(arr: any[]): any { return arr[0]; } function first(arr: T[]): T | undefined { return arr[0]; } interface User { id: string; name: string } const users: User[] = [{ id: '1', name: 'Alice' }]; const uAny = firstAny(users); // any const u = first(users); // User | undefined // uAny.name — any, no checking // u?.name — ✅ User | undefined, narrowed access // ─── map: preserve input → output relation ─── function mapAny(arr: any[], fn: (x: any) => any): any[] { return arr.map(fn); } function mapArr (arr: T[], fn: (x: T) => U): U[] { return arr.map(fn); } const lengthsAny = mapAny(users, u => u.name.length); // any[] const lengths = mapArr(users, u => u.name.length); // number[] // ─── pluck: keyof preservation ─────────────── function pluckAny(arr: any[], key: string): any[] { return arr.map(x => x[key]); } function pluck extends keyof T>(arr: T[], key: K): T[K][] { return arr.map(x => x[key]); } const namesAny = pluckAny(users, 'name'); // any[] const names = pluck(users, 'name'); // string[] const ids = pluck(users, 'id'); // string[] // pluck(users, 'foo'); // ❌ 'foo' not a key // ─── Generic composition ───────────────────── // Generics chain — first + pluck compose: const firstNameChar = first(pluck(users, 'name')); // string | undefined // With any, you'd have to cast at every step. // With generics, types flow automatically. console.log(uAny, u, lengthsAny, lengths, namesAny, names, ids, firstNameChar);
function firstAny(arr) { return arr[0]; } function first(arr) { return arr[0]; } const users = [{ id: '1', name: 'Alice' }]; const uAny = firstAny(users); const u = first(users); function mapAny(arr, fn) { return arr.map(fn); } function mapArr(arr, fn) { return arr.map(fn); } const lengthsAny = mapAny(users, u => u.name.length); const lengths = mapArr(users, u => u.name.length); function pluckAny(arr, key) { return arr.map(x => x[key]); } function pluck(arr, key) { return arr.map(x => x[key]); } const namesAny = pluckAny(users, 'name'); const names = pluck(users, 'name'); const ids = pluck(users, 'id'); const firstNameChar = first(pluck(users, 'name')); console.log(uAny, u, lengthsAny, lengths, namesAny, names, ids, firstNameChar);
Type flows through
With , the element type (User) flows through to the return. With any, it's lost — the caller has to cast.
Key-aware pluck
— the return type depends on which key. pluck(users, 'name') = string[]. With any, it's any[].
Generic composition
Generic functions chain: first(pluck(users, 'name')) stays typed. With any, you'd cast between every call.
Caller's burden
With any, every caller must know the actual type and cast. With generics, the type is inferred — zero cast, full safety.
The pattern: "input type = output type, or related." Whenever you write a function and think "this could work with any type," reach for instead of any. The question to ask: "is the return type related to the parameter type?" If yes (identity, first, last, map, filter, pluck) — generic. If the return type is independent of the parameter type (e.g. log(x): void, parse(x: string): number) — just use the actual parameter type. any fits in neither case.
✅ Solution 3 — Generic React components
The pattern behind every typed data table and select.
Generics shine in React components that render data of any type. A List that renders User[], Post[], Product[] — preserving type information in the render callback — is impossible with any. This is the pattern TanStack Table, react-select, and every typed data component uses.
// ─── With any — loses item type ────────────── function BadList({ items, render }: { items: any[]; render: (item: any) => any; }) { return
- {items.map((item, i) =>
- {render(item)} )}
-
{items.map((item, i) => (
- keyExtractor(item) : i}> {render(item, i)} ))}
- u.id}
render={(u) => {u.name} ({u.email})}
/>
{/* T = Product — item is Product */}
- p.sku}
render={(p) => {p.title} — $`{p.price}}
/>
{/* ❌ Typos caught at compile time: /}
{/ render={(u) => u.nmae} — 'nmae' does not exist on User /}
{/ render={(u) => u.price} — 'price' does not exist on User */}