skip to content

In TypeScript, what does the type `(id: number) => Promise<User>` describe, and must the implementing function name its parameter `id`?

level: juniorimportance: must knowfreq 72%

answer

  1. a shape, not a value
  2. the arrow lives in type position
  3. positions matter, names do not
  4. return type sits after the arrow
  5. colon form belongs inside a member list

basics

~20 s

That type describes any function taking one number argument and returning a Promise of User. Parameter names inside a function type are labels for readers only — matching is positional, so an implementation may name the parameter anything.

solid answer

~50 s

`(id: number) => Promise<User>` is a **function type expression**: it says "a callable taking one `number` and returning `Promise<User>`". It is a type, not a value — the arrow here lives in type position and produces no code, unlike the arrow of an arrow function, which produces a real function. Parameters are matched by **position**, so `const load: (id: number) => Promise<User> = (userId) => fetchUser(userId)` is fine; the name `id` exists only for readability and for editor hints. Note the syntax detail: in this shorthand the return type follows `=>`, so writing `(id: number): Promise<User>` as a standalone type is a syntax error — the colon form is only legal as a call-signature member inside an object type or interface. Like everything in the type layer, the annotation is erased at compile time and checks nothing at runtime.

code

typescript · 12 lines
typescript
interface User {
  id: number;
  name: string;
}

type LoadUser = (id: number) => Promise<User>;

// Parameter renamed: matching is positional, so this is fine.
const load: LoadUser = async (userId) => ({ id: userId, name: 'Ada' });

// Nested function types; parentheses control what the arrow returns.
type MaybeFactory = (() => string) | number;

go deeper

for a junior

Be able to read (id: number) => Promise<User> aloud as "takes a number, returns a promise of User", and say plainly that the parameter name is a label matched by position, not by name.

for a middle

Explain that the arrow in type position produces no emitted code, that assignability compares parameters positionally, and that the return type binds as far right as possible so unions need parentheses.

for a senior

Show judgment about naming: positional matching means two same-typed parameters can be silently swapped, so argument names, ordering, and sometimes an options object are what actually prevent the bug the type cannot catch.

for a principal

Own the convention question — where the codebase declares reusable function types versus inlining them, and how that choice affects the blast radius when a callback signature changes across many call sites.

## What the shorthand says `(id: number) => Promise<User>` is a *function type expression*. It describes the shape of a callable: the parameter list on the left, the return type on the right. Any value with a compatible signature satisfies it; TypeScript is structural, so no declaration of intent is needed. ```ts type LoadUser = (id: number) => Promise<User>; const load: LoadUser = async (userId) => { return { id: userId, name: 'Ada' }; }; ``` The parameter of the arrow function needs no annotation because the annotation on `load` supplies it — the compiler contextually types `userId` as `number`. ## The arrow is in a different world The same `=>` token appears in two unrelated roles. In *value* position it builds a function: `(x) => x + 1` is code that exists after compilation. In *type* position it builds a type: `(x: number) => number` is a description that is deleted from the emitted JavaScript. Beginners routinely read a function type as an arrow function whose body is missing. It is not missing; there is no body in a type. This matters because everything else in the tree follows from it: nothing in a function type survives to runtime, so no argument is checked when the program actually runs. If the value came from `JSON.parse` or an untyped library, the annotation is a claim, not a guarantee. ## Parameter names are documentation Assignability compares parameters **by position**, never by name: ```ts type Handler = (event: string, at: number) => void; const h: Handler = (name, timestamp) => { console.log(name, timestamp); }; ``` This compiles. `name` is the `string`, `timestamp` is the `number`, because they sit in slots one and two. TypeScript has no named-argument calling convention, so the identifiers in a function type serve editors (signature help shows them) and human readers. Choose them well anyway — a type read as `(string, string) => void` tells the next person nothing, while `(userId: string, tenantId: string) => void` prevents a swapped-argument bug at review time. ## Where a function type can appear Anywhere a type is allowed: ```ts function retry(op: () => Promise<void>, times: number): Promise<void> { /* ... */ } interface Config { onError: (error: Error) => void; } type Middleware = (next: (value: string) => string) => (value: string) => string; ``` The last line shows two things worth noticing. A function type nests: a parameter can itself be a function type, and the return type can be a function type. And because the return type extends as far to the right as it can, `() => string | number` means "returns `string | number`", not "either a function or a number" — parenthesise as `(() => string) | number` when you mean the union of a callable and a number. That precedence rule is the source of a very common typo. ## The colon form is a different syntax Inside an object type or an interface, the same callable is written with a colon as a *call-signature member*: ```ts interface LoadUser { (id: number): Promise<User>; } ``` This describes exactly the same callable as the arrow shorthand. The member form exists because a member list can hold *more* members — which is how you describe something that is callable and also carries properties. As a standalone type, though, only the arrow form parses: `type Bad = (id: number): Promise<User>;` is a syntax error. ## What it does not do It does not create a function, does not check arguments at runtime, and does not enforce the parameter names. It also does not describe a constructor — `new`-ing a value typed with a plain call signature is an error, because constructability is written separately. And it does not make the value non-null: under `strictNullChecks` a variable of this type still cannot hold `undefined` unless you say so, but nothing stops an `any` value from being assigned into it and blowing up later. The practical takeaway for an interview: read a function type left to right as "takes these positions, gives back this", say plainly that the names are labels, and say that the whole annotation disappears from the output.

  • The `=>` in a function type and the `=>` in an arrow function look identical — what actually differs?
    Only the position. In value position `=>` builds a real function that exists in the emitted JavaScript. In type position it builds a description with no body and no output: `type F = (x: number) => string` emits nothing at all. Confusing the two leads people to look for a missing function body inside a type alias.
  • Does annotating a parameter as `(id: number) => Promise<User>` cause any runtime check when a caller passes something else?
    No. Types are erased during compilation, so the emitted JavaScript contains no check that the value is callable or that its argument is a number. If the value arrived from `JSON.parse`, a network response, or an untyped module, the annotation is a claim you have made, and a wrong claim surfaces as a runtime `TypeError`, not a compile error.

saying these in an interview costs you the question

  • Believes the implementation must reuse the parameter names from the type
  • Reads a function type as an arrow function whose body was omitted
  • Writes the return type after a colon in a standalone function type
  • Thinks the annotation checks arguments when the program runs
  • Assumes `() => string | number` means either a function or a number

context