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?
answer
- two effects: optional, plus a type
- initializer decides the inferred type
- literal or its widened base type?
- body never sees undefined here
- annotation may be wider than the default
basics
~20 sA 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.
solid answer
~40 sAn initializer does two things to the type layer. First it makes the parameter optional at call sites, so `run()` and `run('slow')` both check, and the emitted declaration shows it as `mode?: string`. Second it supplies the type: `mode = 'fast'` infers `string`, widened from the literal type `'fast'` because a parameter is a mutable binding — use `mode: 'fast' | 'slow' = 'fast'` if you want the narrow set. Inside the body, `mode` is plain `string` with no `undefined`, since the default has already run. You can combine an annotation with a default, and the initializer must be assignable to the annotation. What you cannot do is write both `?` and a default — that is a syntax error, because the initializer already implies optionality.
go deeper
Know that a default lets callers omit the argument and that the compiler takes the parameter's type from the initializer, so mode = 'fast' gives you a string parameter you can use directly.
Explain the widening rule — a mutable binding initialised from a literal infers the base type, not the literal type — and why the in-body type contains no undefined while the declaration emits mode?: string.
Discuss when a default belongs in the signature versus inside the body, and note that a default is real emitted code, so changing it silently changes behaviour for every caller that omitted the argument.
Own defaults as part of an API contract: where the source of truth for a default lives when several layers each supply one, and how to evolve a default without breaking consumers who were relying on the old value.
## Two effects on the type layer Writing `function run(mode = 'fast')` changes the signature in two ways at once. **It makes the parameter omittable.** The compiler's arity check now accepts a call with no arguments, and the declaration emit reflects that: ```ts function run(mode = 'fast') {} // emitted .d.ts: declare function run(mode?: string): void; run(); // ok run('slow'); // ok ``` **It supplies the type by inference.** With no annotation, the parameter's type comes from the initializer. ## Inference widens the literal This is the part candidates get wrong. `mode = 'fast'` does *not* infer the literal type `'fast'`; it infers `string`. ```ts function run(mode = 'fast') { mode = 'anything'; // fine — mode is string } ``` The rule is general to inference: a *mutable* binding initialised from a literal gets the widened base type, because the binding can later hold any other value of that type. If you want the narrow set of allowed values, annotate: ```ts function run(mode: 'fast' | 'slow' = 'fast') { // mode: 'fast' | 'slow' } run('turbo'); // error: not assignable to '"fast" | "slow"' ``` The same widening applies to other literal initializers — `retries = 3` infers `number`, `verbose = false` infers `boolean`. ## Annotation plus default The two can coexist, and then the annotation wins for the parameter's type while the initializer must be assignable to it: ```ts function f(count: number = 0) {} // ok function g(count: number = 'zero') {} // error: string is not assignable to number ``` A useful case is an annotation that is *wider* than the default, as in the union example above. ## Inside the body, there is no `undefined` A parameter with a default is optional to the *caller*, but inside the function it is already resolved — the type does not include `undefined`, and no narrowing is needed: ```ts function run(mode = 'fast') { mode.toUpperCase(); // no error; mode is string } ``` This is the practical advantage of a default over a bare `?`: you handle the missing case once, in the signature, instead of at every use. ## What the caller may pass A parameter with a default accepts the argument being omitted *and* accepts an explicit `undefined`, which triggers the default. It does not accept `null` — `null` is a value, and it will be assigned through unless the annotation permits it and you handle it. (That the default fires for `undefined` and not for `null` is a JavaScript runtime rule about parameter initializers; the type layer simply reflects it.) ```ts function run(mode = 'fast') {} run(undefined); // ok — mode becomes 'fast' run(null); // error under strictNullChecks: null is not assignable to string ``` ## `?` and a default cannot be combined ```ts function f(a?: number = 1) {} // error: parameter cannot have question mark and initializer ``` The initializer already makes the parameter omittable, and it also removes `undefined` from the in-body type — a `?` would contradict the second half. ## A default before a required parameter Unlike `?`, an initializer may appear *before* a required parameter. The parameter is then not optional: callers must fill the slot, passing `undefined` to take the default. ```ts function connect(timeout = 1000, host: string) {} connect(500, 'db'); // ok connect(undefined, 'db'); // ok — timeout falls back to 1000 connect('db'); // error: string is not assignable to number ``` That signature is legal but awkward to call, which is usually a hint to reorder the parameters or take an options object. ## Erasure and emit Unlike almost everything else in the type layer, the default value itself is *not* erased — it is real JavaScript, and it is emitted: ```js function run(mode = 'fast') { } ``` Only the annotation disappears. So a default has a runtime cost and runtime behaviour, while the `?` marker has neither. Note the consequence for a public API: changing a default changes runtime behaviour for every caller that omitted the argument, even though the type signature looks unchanged. ## Common mistakes - Expecting `mode = 'fast'` to infer the literal type `'fast'` and to reject other strings. - Narrowing `mode` inside the body "in case it is undefined" — it cannot be. - Writing `?` alongside an initializer. - Assuming the default fires for any falsy argument. It fires only when the argument is `undefined` or absent, so an explicitly passed `0`, `''` or `null` is not replaced.
- How would you keep the default but have the parameter typed as a narrow union of allowed values?Annotate explicitly: `function run(mode: 'fast' | 'slow' = 'fast')`. The annotation determines the parameter type, the initializer only has to be assignable to it, and callers passing anything outside the union are rejected. Relying on inference alone gives you the widened `string`.
- Does a parameter with a default ever have undefined in its type inside the function body?No. The initializer runs before the body, so the parameter is already resolved and the declared type excludes `undefined` — no narrowing needed. That is the main ergonomic difference from a bare optional parameter, where every use site has to handle the missing case.
- Is a default value erased at compile time like the rest of the type layer?No — the initializer is ordinary JavaScript syntax and is emitted as written. Only the annotation disappears. That also means changing a default is a behavioural change for every caller that omitted the argument, even though the emitted signature and the declared types look unchanged.
saying these in an interview costs you the question
- Says `mode = 'fast'` infers the literal type 'fast'
- Narrows the parameter for undefined inside the body
- Combines a question mark with an initializer
- Claims the default fires for any falsy argument
- Thinks the default value is erased along with the types