Problem Solved

TypeScript · generic vs any

Home › Courses › TypeScript › Problems › Generic vs any
🔷 Problem · 5 min read

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.

identity.ts
// ─── 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('x'). 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 preserves.

✅ 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[].

data-transforms.ts
// ─── 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 pluckextends 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.

generic-component.tsx
// ─── With any — loses item type ──────────────
function BadList({ items, render }: {
items: any[];
render: (item: any) => any;
}) {
return 
    {items.map((item, i) =>
  • {render(item)}
  • )}
; } // Caller: no autocomplete on item // u.name} /> // ↑ any — u.nmae also compiles // ─── With — preserves item type ────────── interface ListProps { items: T[]; render: (item: T, index: number) => React.ReactNode; keyExtractor?: (item: T) => string | number; } function List({ items, render, keyExtractor }: ListProps) { return (
    {items.map((item, i) => (
  • keyExtractor(item) : i}> {render(item, i)}
  • ))}
); } // ─── Usage with full type safety ───────────── interface User { id: string; name: string; email: string } interface Product { sku: string; title: string; price: number } function App() { const users: User[] = [{ id: '1', name: 'Alice', email: 'a@x.com' }]; const products: Product[] = [{ sku: 'X', title: 'Book', price: 10 }]; return (
{/* T = User — item is User */} 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 */}
); } // ─── The pattern behind libraries ──────────── // // TanStack Table: useReactTable() // React Hook Form: useForm() // react-select: /> // Ant Design: columns={...} /> // // Every typed data component uses . // Any component that would use any[] for its // items can be improved with a generic. export default App;
function BadList({ items, render }) {
return React.createElement('ul', null,
items.map((item, i) => React.createElement('li', { key: i }, render(item)))
);
}

function List({ items, render, keyExtractor }) {
return React.createElement('ul', null,
items.map((item, i) =>
React.createElement('li', {
key: keyExtractor ? keyExtractor(item) : i
}, render(item, i))
)
);
}

function App() {
const users = [{ id: '1', name: 'Alice', email: 'a@x.com' }];
const products = [{ sku: 'X', title: 'Book', price: 10 }];

return React.createElement('div', null,
React.createElement(List, {
items: users,
keyExtractor: (u) => u.id,
render: (u) => React.createElement('span', null, u.name, ' (', u.email, ')')
}),
React.createElement(List, {
items: products,
keyExtractor: (p) => p.sku,
render: (p) => React.createElement('span', null, p.title, ' — `$', p.price)
})
);
}

export default App;

Generic props interface

ListProps — items are T[], render receives T. Each caller fills in T based on what they pass.

Inference from props

Passing items={users} infers T = User. The render callback then knows item is User — full autocomplete.

The ecosystem pattern

TanStack Table, React Hook Form, react-select, Ant Design — every typed data component uses this. It's the standard.

Prefer function declarations

function List(...) parses cleanly in .tsx. Arrow functions need with trailing comma to disambiguate from JSX.

⚠️ Common mistakes

The generic-vs-any traps that cost real time.

1. Using any for reusable functions

function first(arr: any[]): any works but loses all type info. Fix: function first(arr: T[]): T — same length, full safety.

2. Using generics when a specific type works

function double(x: T) — but x * 2 fails on non-numbers. Fix: function double(x: number). Generics are for when the type genuinely varies.

3. Constraining too tightly

when you only need id. Fix: — minimal constraint, maximum reuse.

4. Forgetting that T is a type, not a value

if (T === 'x') — T doesn't exist at runtime. Fix: use type guards on values, not on T.

5. Not using keyof for key-based functions

function pluck(arr: T[], key: string): any[] — key isn't checked. Fix: — key must be a real key of T.

6. Casting generic return values

return x as T when T isn't actually narrowed. Fix: structure the function so T's constraints guarantee the return type.

7. Using any in generic constraints

— same as unconstrained. Fix: omit the constraint if you don't have one, or use a real shape.

8. Over-complicating with generics

If the function has 4 type parameters and 12 constraints, no one will read it. Fix: simpler signatures. Sometimes any is fine inside a well-tested boundary.

❓ Frequently asked

What is the difference between a generic and any in TypeScript?

any erases type information — a function returning any loses the input type at the call site. Generics preserve it: identity(x: T): T returns the same type as the input. any means 'I don't know, don't check.' Generics mean 'I don't know yet, but I'll track it.'

When should I use a generic instead of any?

Use a generic when the input type and output type are related — identity, first, last, map, pluck, groupBy. Also when you need to call a method on the input. Use any only for interop with untyped libraries or as a documented temporary escape hatch.

Are generics slower than any?

No. Both are erased at compile time — the output JavaScript is identical. Generics add compile-time checking but zero runtime cost.

Why does my generic function return T instead of the actual type?

Because T is inferred from the argument. When you call identity('hello'), TypeScript infers T = 'hello' (or string, depending on context) and returns that specific type. If the return shows as T in the definition, that's just the generic signature — at the call site, T is filled in. Hover over the call site in your editor to see the resolved type.

🔗 Related

← All TypeScript problems
Problem solved · TypeScript