Generic functions

Declare type parameters on a function so its arguments and return type stay linked at every call site.

Signature
function identity<Type>(arg: Type): Type

Parameters

ParameterTypeDescription
$Typetype parameterA type variable declared in the angle brackets after the function name. It is bound per call, either inferred from the arguments or supplied explicitly. It may carry an `extends` constraint and a default.
$argTypeAn ordinary value parameter whose type is written in terms of the type parameter, which is what lets the compiler infer the type argument from the call.

Return value

TypeWhatever the body returns, described in terms of the type parameters rather than a fixed type; at the call site each type parameter is replaced by the inferred or supplied type argument.

Errors

Nothing at runtime — type parameters are erased. tsc reports error 2345 when an argument does not match the inferred or supplied type argument, and error 2558 when you pass the wrong number of type arguments.

Changelog

  • PHP 3.4Higher-order type inference: passing a generic function into another generic function can now propagate the type parameters through instead of collapsing them to the empty object type `{}` — `compose(makeArray, makeBox)` was inferred as `(arg: {}) => Box<{}[]>` before 3.4 and as `<T>(arg: T) => Box<T[]>` from 3.4 on.
  • PHP 4.7Instantiation expressions let you fix type arguments without calling the function, as in `const numberIdentity = identity<number>;`.
  • PHP 5.0The `const` modifier may be written on a function's type parameters (`<const T>`) to get `as const`-like inference at the call site.

A generic function declares one or more type variables in angle brackets after its name and then uses them in its parameter and return types. The point is the link: function identity<Type>(arg: Type): Type promises the caller gets back exactly what they put in, which any cannot express and overloads express only clumsily.

Inference versus explicit arguments

Most calls need no angle brackets. The compiler infers each type parameter from the arguments, so identity("myString") gives back string. Reach for explicit arguments in three situations:

  • Nothing in the call mentions the parameter — firstOrNull<string>([]) would otherwise infer never.
  • Inference picks something wider or narrower than you want, such as string where you wanted a literal union.
  • The parameter appears only in the return type, so there is no argument to infer from.

What trips people up

Inside the body you know nothing about an unconstrained type parameter. arg.length is an error on Type; either change the parameter to Type[] or add a constraint. The other common surprise is a type parameter used exactly once — if it appears in only one place it is doing no work and unknown would say the same thing more honestly.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Write one array helper that keeps element types instead of returning any[]
  • Type a fetch wrapper so the caller names the response shape once
  • Build a memoize or debounce wrapper that preserves the wrapped function's signature
  • Pass a type argument explicitly when the value is an empty array or null

Frequently asked questions

When should I pass a type argument instead of letting TypeScript infer it?

Pass it when inference has nothing to work with or picks the wrong type — an empty array or null argument, a type parameter that appears only in the return type, or a case where you need a literal union rather than the widened string or number. Every other call reads better without angle brackets.

Why does TypeScript say a property does not exist on my type parameter?

Because an unconstrained type parameter could be anything, so no property is known to exist on it. Add a constraint such as <T extends { length: number }>, or change the parameter type to something structured like T[], and the property becomes visible inside the body.

Are generic functions slower at runtime?

No. Type parameters are erased during compilation, so a generic function emits exactly the same JavaScript as the untyped version — there is no per-type specialisation, no boxing and no dispatch cost.

More examples

Preserving a function's own signature in a wrapper

Args and R are inferred from the function passed in, so loggedAdd has the exact type (a: number, b: number) => number rather than (...args: any[]) => any. The log line prints "add(2, 3) -> 5" and then the returned 5, showing the wrapper is transparent both at runtime and in the type.

Example · typescript
function withLogging<Args extends unknown[], R>(
  name: string,
  fn: (...args: Args) => R
): (...args: Args) => R {
  return (...args: Args): R => {
    const result = fn(...args);
    console.log(`${name}(${args.join(", ")}) -> ${String(result)}`);
    return result;
  };
}

const add = (a: number, b: number): number => a + b;
const loggedAdd = withLogging("add", add);

console.log(loggedAdd(2, 3));

When inference is not enough

Both states hold strings at runtime and the output is "idle busy", but only the second one rejects status.set("pending") at compile time. This is the everyday reason to write the type argument by hand: inference widens a string literal to string unless you tell it otherwise.

Example · typescript
function makeState<T>(initial: T) {
  let value = initial;
  return {
    get: (): T => value,
    set: (next: T): void => {
      value = next;
    },
  };
}

// Inferred as string, so "idle" | "busy" cannot be enforced
const loose = makeState("idle");

// Explicit type argument widens the parameter to the union we want
const status = makeState<"idle" | "busy">("idle");
status.set("busy");

console.log(loose.get(), status.get());

Discussion

  • Be the first to comment on this lesson.