skip to content

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%

answer

  1. which side of the equals sign
  2. who gets to choose T
  3. one value serving many call sites
  4. the alias demands its argument up front
  5. for-any inside versus outside

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.

solid answer

~40 s

The angle brackets sit on different sides of the `=`, and that decides who chooses `T`. `type Mapper = <T>(x: T) => T[]` puts the type parameter on the *call signature*: `Mapper` itself takes no type arguments, and any function assigned to it must work for every `T`, with `T` resolved fresh at each call. `type MapperOf<T> = (x: T) => T[]` puts it on the *alias*: you must write `MapperOf<string>` to use it at all, `T` is pinned at that point, and the function it describes is a plain non-generic one. So `const f: Mapper = (x) => [x]` type-checks and `f(1)` gives `number[]`, while `const g: MapperOf<string> = (x) => [x]` only ever accepts strings. Writing `MapperOf` bare is an error — it requires its type argument.

code

typescript · 11 lines
typescript
type Mapper = <T>(x: T) => T[];
type MapperOf<T> = (x: T) => T[];

// T is chosen per call
const wrap: Mapper = (x) => [x];
const nums: number[] = wrap(1);
const strs: string[] = wrap("a");

// T is fixed where the annotation is written
const wrapString: MapperOf<string> = (x) => [x];
const more: string[] = wrapString("a");

go deeper

for a junior

Recognise both shapes when you read them and know that only the form with brackets on the alias name requires you to write a type argument when you use it.

for a middle

Explain that brackets left of the equals mean the type's user picks T, brackets right of it mean the function's caller picks it, and show what each accepts.

for a senior

Choose deliberately in an API: defer T to the call site when one stored value must serve many types, and pin it on the alias when the type is settled at declaration.

for a principal

Frame it as where the quantifier lives in your public contract — deferring T keeps a single shared value flexible, while pinning it pushes the type argument onto every consumer and spreads through their signatures.

## Two places a type parameter can live When you name a function type, there are two independent slots for a type parameter, and they mean different things: ```ts type Mapper = <T>(x: T) => T[]; // parameter on the call signature type MapperOf<T> = (x: T) => T[]; // parameter on the alias ``` The mechanical rule is where the brackets sit relative to the `=`. Left of the `=`, attached to the alias name, means *the code that writes the type* supplies `T`. Right of the `=`, attached to the signature, means *the code that calls the function* supplies `T`. ## The generic call signature `Mapper` is not a generic type — it takes no type arguments, and `Mapper<string>` is an error because there is nothing to fill. What it describes is a single function value that is itself generic. To satisfy it, a function must be valid for **every** `T`: ```ts const wrap: Mapper = (x) => [x]; const nums: number[] = wrap(1); const strs: string[] = wrap("a"); ``` The parameter `x` needs no annotation: it is contextually typed by the signature, so it is `T`. Each call resolves `T` independently, which is why one value serves both lines above. The flip side is that the bar for assignment is high. A concrete function is **not** assignable: ```ts declare const onlyStrings: (x: string) => string[]; // const bad: Mapper = onlyStrings; // Error: Type '(x: string) => string[]' is not assignable to type 'Mapper'. ``` That rejection is the whole point. `Mapper` promises "works for anything"; a string-only function cannot keep that promise, so the checker refuses. ## The generic alias `MapperOf` is a generic type: a small function at the type level that takes a type and produces a type. It cannot appear on its own — every use must supply the argument, and `MapperOf<string>` expands to `(x: string) => string[]`. The function value it describes is entirely ordinary; nothing about it is generic: ```ts const g: MapperOf<string> = (x) => [x]; g("a"); // g(1); // Error: Argument of type 'number' is not assignable to parameter of type 'string'. ``` Here `T` was fixed at the moment you wrote the annotation, by the author of the annotation, not by the caller. ## Choosing between them Ask who legitimately knows the type. If a single stored function value must serve callers of many different types — a generic identity, a generic `pipe`, a generic memoiser you hand around — you need the generic call signature, because the choice must be deferred to each call. If the type is decided once, at the point where the variable, field or parameter is declared — a `Comparator<User>` field on a sorted list, a `Validator<FormValues>` handed to a form — the generic alias is the right tool, and it keeps the function value itself simple. A common mistake is to reach for the alias form and then discover that every consumer has to thread the type argument through by hand, when what was actually wanted was one value that adapts per call. ## Where a generic call signature can appear The signature form is not limited to type aliases. Anywhere a call signature is legal, the type parameter list may sit on it: as a member of an interface or object type, and as a method signature. ```ts interface Wrapper { wrap<T>(x: T): T[]; // generic method signature wrapFn: <T>(x: T) => T[]; // property whose type is a generic function } ``` Both members are generic per call; the interface itself takes no type arguments. Contrast that with `interface Wrapper<T> { ... }`, where the interface is the thing parameterised. ## Both are erased Neither form leaves a trace in the emitted JavaScript. A type alias emits nothing at all, generic or not, and a type parameter list on a signature emits nothing either. The distinction is purely about what the checker will accept: which side of the `=` the brackets sit on changes who is obliged to supply `T`, and therefore which functions are assignable and what each call returns. Nothing about it is observable at runtime, so you can reshape one into the other freely without touching behaviour. ## The quick test Read the type aloud. "`Mapper` is a function that, for any type you like, takes one of those and gives back an array of them." versus "`MapperOf<T>` is, for a given `T`, a function taking a `T` and giving back an array of `T`." The first sentence has the "for any" inside the function; the second has it outside.

  • Can you assign `(x: string) => string[]` to a variable of type `<T>(x: T) => T[]`?
    No. The target promises a function that works for every `T`, and a string-only function cannot honour that, so the checker reports the parameter types as incompatible. The reverse direction is fine: a generic function is assignable to a concrete signature, because it can be instantiated at that specific type.
  • Besides a type alias, where else can a generic call signature appear?
    Anywhere a call signature is legal: as a method signature in an interface or object type, `wrap<T>(x: T): T[]`, or as a property whose type is a generic function, `wrapFn: <T>(x: T) => T[]`. In both cases the containing interface stays non-generic while the individual member is generic per call.
  • Is `Mapper<string>` valid for `type Mapper = <T>(x: T) => T[]`?
    No — `Mapper` takes no type arguments, so supplying one is an error. The type parameter belongs to the call signature, not the alias, so you pick `T` by calling the function, not by writing it into the annotation. Only the `MapperOf<T>` form accepts a type argument.

saying these in an interview costs you the question

  • Says the two forms are just different syntax for the same type
  • Thinks Mapper<string> is valid when T sits on the signature
  • Assumes a concrete function is assignable to a generic signature
  • Believes the alias form defers T to the call site
  • Claims one of the two forms emits runtime code

context