skip to content

Generic Functions & Methods

Writing a function whose parameter and return types are linked by a type variable, rather than duplicating it per type or falling back to any. Almost every generics question starts here, usually with identity, map, or a wrapper around fetch.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

questions

4

In TypeScript, what does declaring `function identity<T>(x: T): T` give you that `function identity(x: any): any` does not?

level: juniorimportance: must knowfreq 84%

answer

  1. one function, many call sites
  2. relationship between argument and result
  3. any switches the checker off entirely
  4. T remembers, unknown forgets
  5. both emit identical JavaScript

basics

~20 s

A type parameter links the argument type to the return type, so the call site keeps the concrete type it passed in and stays type-checked. any turns checking off instead: the result is unchecked and any misuse of it silently compiles.

solid answer

~40 s

`any` is an escape hatch: it switches checking off for that value, so the call returns `any` and every downstream use of the result compiles — including the wrong ones. A type parameter is the opposite. `T` is a variable standing for whatever type the caller actually used, so it links the parameter to the return type. Call `identity("hi")` and the checker resolves `T` to `string`, so the result is a `string` and calling `.toFixed()` on it is an error. The generic version states something true about the function — whatever goes in comes back out — instead of giving up. Both erase to the same JavaScript, `function identity(x) { return x; }`, so nothing about `T` survives compilation: the safety is free at runtime, and you cannot inspect `T` inside the body.

code

typescript · 11 lines
typescript
function identityAny(x: any): any {
  return x;
}

function identity<T>(x: T): T {
  return x;
}

const a = identityAny("hello"); // a: any — every use of a is unchecked
const b = identity("hello");    // b: string — checked from here on
const c = identity(42);         // c: number

go deeper

for a junior

Be able to write the identity signature from memory and say plainly that T ties the return type to the argument type, while any turns checking off for everything downstream.

for a middle

Explain that T is resolved per call site from the argument, that the body is checked once for all possible T, and that nothing is known about T unless you say so.

for a senior

Show why an any in a shared helper is a defect that spreads: the unchecked type propagates to every consumer of the result, so the bug surfaces far from the function that caused it.

for a principal

Own the API-surface argument: a signature is a contract, and a type parameter documents an invariant that reviewers and tooling can enforce, while any silently opts a whole subtree of the codebase out of checking.

## The problem Some functions are genuinely shaped the same for every type they touch: return the first element of an array, wrap a value in an object, pass a value through a logger. Without type variables you have three bad options — write the function once per type, widen everything to `any`, or widen everything to `unknown`. A type parameter is the fourth option: declare a placeholder in the signature and let each call site fill it in. ## What `any` actually does `any` is not "some type I don't know". It is an instruction to the checker to stop checking. A value of type `any` is assignable to everything, everything is assignable to it, and every property access and call on it is allowed. So with ```ts function identityAny(x: any): any { return x; } const a = identityAny("hello"); a.toFixed(2); // compiles; throws at runtime a.whateverYouLike; // compiles too ``` the error is not reported at the function — it is reported nowhere. Worse, `any` is contagious: `a` is `any`, so anything derived from `a` is `any` as well, and the hole spreads outward through the code that consumes the result. ## What the type parameter does instead `function identity<T>(x: T): T` declares one type parameter, `T`, in the angle brackets after the function name. Inside the signature `T` is an ordinary type you can use in parameters, in the return type, and in annotations in the body. The key point is that `T` occurs twice: once on the parameter and once on the return type. That single repetition is the whole payload — it tells the checker that the return type is *the same type as the argument*, whatever that turns out to be. ```ts function identity<T>(x: T): T { return x; } const b = identity("hello"); // b: string const c = identity(42); // c: number // b.toFixed(2); // Error: Property 'toFixed' does not exist on type 'string' ``` The function body is also checked against `T`. Because `T` could be any type, `x.length` inside the body is an error — you have said nothing about what `T` supports, so the checker assumes nothing. That is a feature: the body is verified once, for all possible callers, rather than re-verified per call. ## Everything is erased TypeScript's whole type layer disappears during compilation. The emitted JavaScript for both versions is identical: ```js function identity(x) { return x; } ``` There is no per-type copy of the function (unlike C++ templates), no runtime type token, and no reflection. Consequences worth saying out loud in an interview: a generic function costs nothing at runtime; you cannot write `if (x instanceof T)` in the body, because `T` is not a value; and you cannot switch on `T` to pick behaviour. If the body genuinely needs to branch on the shape of the value, it has to test the *value* with a runtime check, not the type parameter. ## `unknown` is safe but forgetful `function identity(x: unknown): unknown` is type-safe in a way `any` is not — the checker refuses every operation on the result until you narrow it. But safety is not the same as precision. `unknown` throws the caller's information away, so every call site has to prove all over again what it already knew. A type parameter keeps that information: `identity("hi")` gives back a `string` with no narrowing, no assertion, and no ceremony. The rule of thumb: `unknown` for values whose type you truly do not know (parsed JSON, a caught error); a type parameter for values whose type the *caller* knows and you merely pass through. ## The call site Usually you write nothing extra — the checker works `T` out from the argument. You can also supply it explicitly with `identity<string>("hi")`, which is occasionally useful to pin down a wider type than the argument suggests or to make a signature's intent obvious at a call site. Explicit type arguments never change the emitted code; they only constrain what the checker accepts. ## What it does not buy you A type parameter is not validation. It does not check that the incoming value really is a `string`, because there is no runtime check to perform. It also does not create a relationship you did not write: if `T` appears only once in the whole signature, the function is not really generic in a useful way — the parameter is doing no linking work, and the honest signature is a concrete type or `unknown`.

  • Does adding a type parameter to a function cost anything at runtime?
    Nothing. Type parameters are erased during compilation, so the generic and the `any` version emit byte-for-byte the same JavaScript. There is no specialised copy per type as in C++ templates and no runtime type token. The only TypeScript constructs that emit runtime code are `enum` and legacy decorators.
  • Inside the body of `function identity<T>(x: T)`, can you write `if (x instanceof T)`?
    No. `T` is a type, not a value, and it does not exist after compilation, so it cannot appear in an expression position — the compiler rejects it. If the body must branch on the shape of the value, test the value itself with `typeof`, `instanceof` against a real class, or a property check.
  • If `any` is the unsafe option, why not just use `unknown` everywhere instead of a type parameter?
    `unknown` is safe but forgetful: the checker blocks every operation until the caller narrows, so information the caller already had is thrown away and must be recovered by hand. A type parameter keeps it — the result comes back as the exact type that went in. Use `unknown` when the type genuinely is not known, such as parsed JSON.

saying these in an interview costs you the question

  • Says any and a type parameter are basically interchangeable
  • Claims generics add runtime cost or generate one function per type
  • Thinks T can be inspected at runtime with instanceof
  • Believes the generic version validates that the argument matches T
  • Says you must always pass the type argument explicitly at the call site

context

open as a page

In TypeScript, what is the difference between `type Mapper = <T>(x: T) => T[]` and `type MapperOf<T> = (x: T) => T[]`?

level: middleimportance: should knowfreq 41%

basics

~20 s

Position decides who picks T. In the first, the type takes no arguments and describes a generic function whose T is chosen fresh at each call. In the second, the type itself takes an argument and describes an ordinary non-generic function.

open as a page

In a `.tsx` file, `const identity = <T>(x: T) => x;` fails to compile even though the identical line is fine in a `.ts` file. Why, and how do you write a generic arrow function there?

level: middleimportance: should knowfreq 52%

basics

~20 s

In .tsx files the parser reads <T> as the opening tag of a JSX element, not a type parameter list, so the line never parses as a generic arrow. Disambiguate with a trailing comma, <T,>, with a constraint, or by using a function declaration.

open as a page

When modelling a TypeScript function whose result type depends on its argument, when would you reach for a type parameter rather than writing a set of overload signatures?

level: seniorimportance: should knowfreq 37%

basics

~20 s

Use a type parameter when one uniform rule relates argument to result across an open set of types, and overloads only when a small fixed set of unrelated argument shapes produce unrelated results. Overloads also break on union arguments and on unlisted types.

open as a page