skip to content

Function Annotations

How you describe a function to the type system: what it takes, what it gives back, and which parameters are optional or variadic. Interviewers use these questions to check that you rely on contextual typing and inference instead of annotating every callback by hand.

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

questions

10

In TypeScript, what does the question mark do in `function log(msg: string, level?: string)`, and what rules govern where such a parameter may appear?

level: juniorimportance: must knowfreq 70%

answer

  1. caller may leave it out
  2. type gains undefined inside
  3. position matters in the list
  4. cannot pair with an initializer
  5. erased — no runtime arity check

basics

~20 s

A question mark makes the parameter optional: callers may omit it, and inside the body its type includes undefined. Optional parameters must follow all required ones, and a parameter cannot have both a question mark and a default value.

solid answer

~40 s

`level?: string` marks the parameter optional. At the call site the compiler's arity check accepts `log('hi')` as well as `log('hi', 'warn')`, and it also accepts `log('hi', undefined)`. Inside the function body, with `strictNullChecks` on, `level` has type `string | undefined`, so you must narrow it — a truthiness check, an `=== undefined` check, or a `??` fallback — before using it as a string. Two syntax rules apply: every optional parameter has to come after all required ones, so `function f(a?: string, b: number)` is rejected, and you cannot combine `?` with an initializer — `function f(a?: string = 'x')` is an error, because a default already implies optionality. The `?` is pure type-layer syntax: it is erased, and the emitted JavaScript is just `function log(msg, level) { ... }`.

go deeper

for a junior

Know that ? lets the caller omit the argument, that the value is then undefined, and that optional parameters come last. Be ready to add a ?? fallback when the compiler complains the value may be undefined.

for a middle

Explain the mechanics: the parameter's declared type becomes T | undefined under strictNullChecks, the arity check is compile-time only, and ? plus an initializer is a syntax error because a default already implies optionality.

for a senior

Show judgment about when optionality belongs in the signature at all. Discuss what happens when values arrive from untyped or external code, and why an optional parameter is a caller-side convenience rather than any kind of validation.

for a principal

Own the API-shape tradeoff: a growing tail of optional parameters is a signal to move to an options object, which keeps call sites readable and lets fields evolve independently. Weigh that against the churn of changing a widely-used signature.

## What the question mark actually declares In a TypeScript parameter list, `?` after the parameter name declares that **callers may omit the argument**. It changes two things: the *minimum arity* the compiler will accept at call sites, and the *type* the parameter has inside the body. ```ts function log(msg: string, level?: string) { // level: string | undefined } log('started'); // ok — one argument log('started', 'info'); // ok — two arguments log('started', undefined); // ok — undefined is an acceptable value ``` With `strictNullChecks` enabled (which `strict` turns on), the declared type of `level` is `string | undefined`. That is the whole point: the compiler refuses to let you treat a possibly-missing argument as if it were always there. ```ts function log(msg: string, level?: string) { console.log(level.toUpperCase(), msg); // error: 'level' is possibly 'undefined' } ``` You fix it by narrowing or by supplying a fallback: ```ts function log(msg: string, level?: string) { const l = level ?? 'info'; // l: string if (level !== undefined) { level.toUpperCase(); // narrowed to string here } } ``` ## Ordering: optional parameters go last A required parameter cannot follow an optional one. The reason is positional: arguments are matched by position, so if an earlier slot could be skipped there would be no way to tell which value landed where. ```ts function bad(a?: string, b: number) {} // error: a required parameter cannot follow an optional parameter function good(a: string, b?: number) {} // fine ``` Several optional parameters may follow each other, and callers then omit them from the right: `function f(a: string, b?: number, c?: boolean)` accepts one, two, or three arguments, but there is no syntax for "skip `b`, pass `c`" other than passing `undefined` explicitly for `b`. ## `?` and a default are mutually exclusive ```ts function f(a?: number = 1) {} // error: parameter cannot have question mark and initializer function f2(a = 1) {} // fine — the initializer already makes it optional ``` An initializer makes the parameter omittable *and* removes `undefined` from its type inside the body, so writing `?` on top of it would be contradictory. ## The arity check is compile-time only This is the erasure rule that organises all of TypeScript: none of this survives to runtime. The emitted JavaScript is ```js function log(msg, level) { /* ... */ } ``` There is no check that a caller supplied the right number of arguments — plain JavaScript has always let you call any function with any number of arguments and binds the missing ones to `undefined`. The `?` is a *promise the compiler enforces on the callers it can see*. Code that reaches the function from untyped JavaScript, a `any`-typed value, or a JSON payload can pass anything at all, so an optional parameter is not a validation mechanism. ## In declaration output and in function types The same `?` syntax appears in function *type* positions and in emitted `.d.ts` files: ```ts type Logger = (msg: string, level?: string) => void; const l: Logger = (m) => console.log(m); // fine — see arity assignability ``` So when you read a signature with a `?`, you are reading the same contract whether it came from your source or from a library's declaration file. ## Optional parameter vs `| undefined` — a real distinction Declaring `level?: string` is *not* the same as declaring `level: string | undefined`. Both permit `undefined` as a value, but only the first lets the caller leave the argument out entirely; the second still requires an argument in the list. That difference is the usual follow-up to this question. ## Common mistakes - Assuming the parameter is still `string` inside the body. Without narrowing, the compiler flags every use. - Writing `?` on a middle parameter and then adding a required one after it. - Believing the arity rule is enforced at runtime, or that omitting an argument produces anything other than `undefined`. - Reaching for `?` when the API really wants an explicit choice from the caller — if forgetting the argument is a bug, make it required.

  • How would you let a caller skip a middle parameter while still passing the last one?
    You cannot skip it positionally — the caller has to pass `undefined` explicitly in that slot, as in `f('a', undefined, true)`. If that reads badly, the signature is telling you to switch to a single options object, where each field can be omitted independently and named at the call site.
  • Does adding `?` to a parameter of an already-published function signature break existing callers?
    No — every call that previously supplied the argument still type-checks, since the parameter's type still accepts the value it was given. Making a required parameter optional is a widening change on the caller side. Going the other way, removing the `?`, breaks every caller that omitted it.
  • If `?` is erased, what stops a JavaScript caller from passing the wrong number of arguments?
    Nothing. The arity check exists only where the compiler can see the call. Untyped JavaScript, an `any`-typed reference, or a dynamic dispatch can pass any number of arguments, and the extras are silently ignored while the missing ones are `undefined`. Validate at trust boundaries rather than relying on the signature.

saying these in an interview costs you the question

  • Claims the parameter's type stays string inside the body
  • Thinks TypeScript checks argument count at runtime
  • Puts a required parameter after an optional one
  • Writes both a question mark and a default value
  • Says optional and `| undefined` are interchangeable at call sites

context

open as a page

In TypeScript, the function `function add(a, b) { return a + b; }` is rejected under the `noImplicitAny` compiler option, yet the same function needs no return type annotation at all. Why does the compiler treat parameters and return types so differently?

level: juniorimportance: must knowfreq 82%

basics

~20 s

TypeScript infers a return type from the function body, but nothing tells it what callers will pass, so unannotated parameters fall back to the implicit any type, which noImplicitAny rejects. Annotate the inputs; let the body infer the output.

open as a page

In TypeScript, why is a function that takes fewer parameters assignable where one taking more is expected, and what bug can that allow through?

level: seniorimportance: must knowfreq 52%

basics

~20 s

A function that ignores trailing parameters can safely stand in for one that receives them, so TypeScript allows fewer parameters but never more. The risk is that a callback silently receives extra arguments it does not expect, which then land in a parameter that has a default.

open as a page

In TypeScript, when is it worth writing an explicit return type annotation on a function, and when is letting the compiler infer it the better choice?

level: seniorimportance: must knowfreq 62%

basics

~20 s

Annotate the return type when the result should be checked against a contract you chose: exported API, recursive functions, and anywhere a wrong result must be reported at the definition rather than at distant call sites. Let local helpers and callbacks infer.

open as a page

In TypeScript, what does giving a parameter a default value do to the function's type — what is inferred for `mode` in `function run(mode = 'fast') {}`, and is the parameter optional?

level: middleimportance: should knowfreq 58%

basics

~20 s

A default makes the parameter omittable and lets the compiler infer its type from the initializer. mode is inferred as string, not the literal 'fast', because inference widens literals in mutable positions. Inside the body the type excludes undefined.

open as a page

In TypeScript, what is the difference between declaring a function parameter as `x?: number` and declaring it as `x: number | undefined`?

level: middleimportance: should knowfreq 55%

basics

~20 s

Both allow the value undefined, but only x?: number lets the caller omit the argument. With x: number | undefined the argument is still required, so the caller must write undefined explicitly. Inside the body both have type number | undefined.

open as a page

In TypeScript, how do you type a rest parameter, and what changes when you type it as a tuple instead of an array?

level: middleimportance: should knowfreq 42%

basics

~20 s

A rest parameter must be typed as an array or a tuple, and must be the last parameter. An array type such as ...args: number[] accepts any number of arguments; a tuple type such as ...args: [x: number, y: number] pins the exact arity and names each position.

open as a page

In TypeScript, what return type is inferred for `async function loadCount() { return 1; }`, and what happens if you annotate that function's return type as `number`?

level: middleimportance: should knowfreq 44%

basics

~10 s

The inferred return type is Promise<number>. Annotating it as number is a compile error: an async function's declared return type must be a promise type, so the correct annotation is Promise<number>.

open as a page

In TypeScript, `['a', 'bb'].map(s => s.length)` compiles with no annotation on `s`, but `const len = (s) => s.length;` is an error under `noImplicitAny`. What is the difference between the two positions?

level: middleimportance: should knowfreq 60%

basics

~20 s

Contextual typing. The callback sits in a position whose expected type is already known — the declared signature of the map callback — so its parameter takes its type from there. The standalone arrow has no expected type, so nothing constrains its parameter.

open as a page

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%

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.

open as a page