skip to content

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