skip to content

A TypeScript codebase declares `type Logger = (string) => void;` and, oddly, values of that type accept an argument of any type at all. What does that type expression actually mean, and how should it be written?

level: middleimportance: should knowfreq 48%

answer

  1. read it as a parameter list
  2. a bare identifier there is a name
  3. what an unannotated parameter becomes
  4. positions decide assignability, not names
  5. arrow standalone, colon inside an object type

basics

~20 s

In a function type expression every parameter must be named and annotated separately. (string) => void declares one parameter named string whose type is implicitly any — not a string parameter. The correct form is (message: string) => void.

solid answer

~50 s

A function type expression uses parameter-list syntax, so what looks like a type in `(string) => void` is parsed as a parameter *name*. The type declares a single parameter called `string` with no annotation, which makes it implicitly `any` — hence values of `Logger` swallow any argument. Under `noImplicitAny` you get an error pointing at that implicit any, which is the usual way people discover the mistake; with the flag off it is completely silent, and that is the dangerous case. The right form is `type Logger = (message: string) => void;`. The parameter name there is documentation only: assignability between function types is decided positionally, so `const log: Logger = (text) => { ... }` is fine and `text` is contextually typed as `string`. Note the other syntax detail people trip on — a standalone function type separates its return type with `=>`, while a method written inside an object type uses `:`.

code

typescript · 7 lines
typescript
type Logger = (message: string) => void;

const log: Logger = (text) => {
  console.log(text.toUpperCase());
};

log("ready");

go deeper

for a junior

Know the shape (name: Type) => ReturnType and that every parameter needs both a name and an annotation; a lone identifier is a name, not a type.

for a middle

Explain why (string) => void compiles and what it means, and that function-type assignability is positional, so the implementation may rename parameters or declare fewer of them.

for a senior

Connect it to project hygiene: this hole is silent without noImplicitAny, so a type alias meant to tighten a boundary can be the thing that loosens it, and strict-mode enforcement is the systematic fix.

for a principal

Own the convention question — where shared function types live, when a named alias beats an inline signature, and how well-named parameters in those types pay back across every editor hint in the codebase.

## The syntax, precisely A function type expression is written like a parameter list followed by an arrow and a return type: ```ts type Handler = (event: string, retries: number) => void; ``` Every parameter needs **both** a name and a type annotation. That requirement is what produces the trap. `(string) => void` is syntactically valid — it is a parameter list containing one parameter whose *name* is `string`: ```ts type Bad = (string) => void; // one parameter named "string", type implicitly any type Good = (value: string) => void; // one parameter of type string ``` The distinction is invisible if you read it as "a function taking a string", which is exactly how the same shape reads in several other languages. TypeScript needs the names because the same grammar supports optional parameters, rest parameters and destructuring, all of which attach to a named binding. ## Why it is silent, and when it is not `Bad`'s parameter has no annotation, so it is an implicit `any`. Under `noImplicitAny` — part of `strict` — the compiler reports it at the type declaration itself, which is why strict projects catch this immediately. With the flag off, nothing is reported: the type compiles, every call passes type checking whatever you hand it, and the checker silently stops helping at that boundary. This is one of the standard arguments for turning `noImplicitAny` on before a codebase accumulates type aliases like this one. ## Parameter names are labels Once the type is written correctly, the names in it carry no checking weight. Assignability between function types compares parameters **by position**: ```ts type Handler = (event: string, retries: number) => void; const onFail: Handler = (message, attempts) => { // message: string, attempts: number — from the position, not from the name }; ``` The implementation may rename freely, and it may declare **fewer** parameters than the type promises — a function that ignores trailing arguments is safe wherever more are supplied. The names still matter to humans: they appear in editor hints and signature help, so `(event: string, retries: number)` is materially better documentation than `(a: string, b: number)`. Because the type supplies parameter types contextually, repeating the annotations in the implementation is redundant, and a re-annotation that is wider than the contract quietly weakens the body. ## Where else the same shape appears The arrow form is one of several ways to say "callable". Written as a member inside an object type, the same idea uses a colon: ```ts type Fn = (n: number) => string; // function type expression type Obj = { format(n: number): string }; // method member inside an object type ``` Mixing the two up produces confusing errors — `type Fn = (n: number): string;` does not parse. Remember: a standalone function type uses `=>`; a member inside an object type uses `:`. A function type expression can be used anywhere a type is expected, not only in an alias: ```ts function retry(op: () => Promise<void>, onError: (e: unknown) => void) { /* ... */ } ``` That inline form is idiomatic for a one-off callback; give it a name with `type` when the same shape appears in several places, or when the name itself carries meaning (`Reducer`, `Validator`, `Middleware`). ## Small details worth knowing - The return type is mandatory in a function type expression; there is no body to infer from, so you must write `=> void`, `=> string`, and so on. - A parameterless function type is `() => T`; the empty parameter list is required. - Because parameter names are just bindings, optional (`?`) and rest parameters are written inside the type exactly as in a real signature. - The whole construct is erased. A `Logger` value is an ordinary JavaScript function at runtime with no argument checking whatsoever; the type only constrains what the compiler will let you pass. ## The takeaway Read a function type as a parameter list, not as a tuple of types. Every parameter position needs `name: Type`. A bare identifier there is a name, and a name without an annotation is an `any` — the exact hole the type alias was supposed to close.

  • If parameter names in a function type do not affect checking, why bother choosing good ones?
    They are the documentation the editor shows. Signature help, quick info and autocompletion all display the declared names, so `(event: string, retries: number)` tells a reader what to pass while `(a: string, b: number)` tells them nothing. They also appear in error messages, which quote the signature verbatim.
  • Can a value typed as `(a: string, b: number) => void` be implemented with only one parameter?
    Yes. A function that declares fewer parameters is assignable to a type declaring more — the extra arguments are passed and ignored, which is always safe. The reverse is not allowed: an implementation demanding more parameters than the type promises would read arguments no caller supplies.
  • What is the difference between writing a function type as `(n: number) => string` and as an object type with a callable member?
    For a plain callable they mean the same thing. The object-type form is what you need when the callable also carries properties — a function that additionally exposes a `.cache` field, say — or when you want to declare more than one signature, since a function type expression describes exactly one.

saying these in an interview costs you the question

  • Reads `(string) => void` as a function taking a string
  • Thinks parameter names in a function type must match the implementation's
  • Believes an unannotated parameter inside a type alias is an error even without noImplicitAny
  • Writes `(n: number): string` for a standalone function type
  • Says the type checks arguments when the function is called at runtime

context