TypeScript Optional Default Parameters

JavaScript functions never complain when you call them with too few or too many arguments — missing parameters simply become undefined. TypeScript keeps that flexibility but lets you be explicit about it: you can mark a parameter as optional with ?, or give it a default value that kicks in whenever the caller omits the argument (or passes undefined). Both features exist to make function signatures honest about which arguments are truly required, while still catching typos and missing arguments the plain-JS way never could.

Overview / How it works

By default, every parameter in a TypeScript function signature is required. If a function is declared as function greet(name: string), the compiler rejects any call that doesn’t supply exactly one string argument. Optional and default parameters are the two ways to opt out of that requirement for a specific parameter.

An optional parameter, written with a trailing ? before the colon (e.g. greeting?: string), tells the compiler \”the caller may omit this argument.\” Inside the function body, TypeScript widens the parameter’s type to include undefined automatically — a parameter declared as greeting?: string has the type string | undefined inside the function, even though you only wrote string. This is exactly why libraries and your own code should check for undefined (with if, ??, or optional chaining) before using an optional parameter’s value.

A default parameter goes one step further: instead of leaving the value as undefined, you supply a fallback value directly in the signature with =, e.g. role: string = \"member\". When the caller omits the argument, or explicitly passes undefined, TypeScript (via the compiled JavaScript) substitutes the default. Crucially, a default parameter is also optional from the caller’s point of view — you never need to (and, as you’ll see below, are not allowed to) add a ? to a parameter that already has a default.

Default values also feed TypeScript’s type inference. If you don’t write an explicit type annotation on a default parameter, the compiler infers the parameter’s type from the default value itself:

function multiply(a: number, b = 2): number {\n  return a * b;\n}\n\nconsole.log(multiply(5));\nconsole.log(multiply(5, 3));

Output:

10\n15

Here b has no explicit annotation, but because its default is 2 (a number literal), TypeScript infers b: number — passing a string for b would be a type error just as if you’d written the annotation yourself.

Syntax

function fnName(\n  required: Type1,\n  optional?: Type2,\n  withDefault: Type3 = defaultValue\n): ReturnType {\n  // body\n}
Piece Meaning
required: Type1 A normal, required parameter. Must be supplied on every call.
optional?: Type2 The ? marks the parameter optional. Its type inside the function becomes Type2 | undefined.
withDefault: Type3 = defaultValue Supplying = defaultValue makes the parameter optional and substitutes defaultValue whenever it’s omitted or passed as undefined.

One strict rule governs ordering: all required parameters must come before optional and default parameters (with the single exception that a required parameter may follow a default parameter only if you explicitly pass undefined for the earlier one at every call site — in practice, just keep required parameters first). We’ll see the error this produces in Common Mistakes below.

Examples

Example 1: A truly optional parameter

function greet(name: string, greeting?: string): string {\n  if (greeting) {\n    return `${greeting}, ${name}!`;\n  }\n  return `Hello, ${name}!`;\n}\n\nconsole.log(greet(\"Ada\"));\nconsole.log(greet(\"Ada\", \"Good morning\"));

Output:

Hello, Ada!\nGood morning, Ada!

greeting can be left out entirely. Inside the function, its type is string | undefined, so the if (greeting) check is required before using it as a plain string — TypeScript would flag a direct use of greeting where a non-optional string is expected.

Example 2: A default parameter

function createUser(name: string, role: string = \"member\"): { name: string; role: string } {\n  return { name, role };\n}\n\nconsole.log(createUser(\"Grace\"));\nconsole.log(createUser(\"Grace\", \"admin\"));

Output:

{ name: 'Grace', role: 'member' }\n{ name: 'Grace', role: 'admin' }

Unlike Example 1, role is never undefined inside the function body — its type is plain string, because TypeScript knows the default guarantees a real value whenever the argument is omitted.

Example 3: Combining defaults with destructured options

interface RequestOptions {\n  timeoutMs?: number;\n  retries?: number;\n}\n\nfunction fetchData(\n  url: string,\n  { timeoutMs = 5000, retries = 3 }: RequestOptions = {}\n): string {\n  return `Fetching ${url} with timeout=${timeoutMs}ms, retries=${retries}`;\n}\n\nconsole.log(fetchData(\"/api/users\"));\nconsole.log(fetchData(\"/api/orders\", { retries: 1 }));\nconsole.log(fetchData(\"/api/orders\", { timeoutMs: 10000, retries: 0 }));

Output:

Fetching /api/users with timeout=5000ms, retries=3\nFetching /api/orders with timeout=5000ms, retries=1\nFetching /api/orders with timeout=10000ms, retries=0

This is the pattern real-world APIs use for an \”options bag\”: the whole second parameter is optional (defaulting to {}), and each property inside it is individually optional with its own default via destructuring. Note that passing { retries: 1 } still gets timeoutMs: 5000 — defaults apply per-property, not all-or-nothing.

Under the hood

Types are a compile-time-only concept — TypeScript erases every annotation, ?, and interface when it compiles to JavaScript. What survives for default parameters is the runtime behavior: the compiler emits a plain JavaScript default parameter (or, when targeting very old JS, an explicit if (x === undefined) x = default check). Roughly, createUser from Example 2 compiles down to something like this:

function createUser(name, role) {\n    if (role === void 0) { role = \"member\"; }\n    return { name: name, role: role };\n}

Output: (illustrative only — this is what the compiled JavaScript looks like, not something you write by hand)

Two consequences follow from erasure: first, at runtime there is no way to ask \”was this argument actually passed, or did the default fill it in?\” — both cases look identical to the function body. Second, in the generated .d.ts declaration file, a default parameter is written with a ? just like a true optional parameter (e.g. role?: string), because from a caller’s perspective a default parameter and an optional parameter are indistinguishable — both can be omitted.

Common Mistakes

Mistake 1: A required parameter after an optional one

function bad(a?: number, b: number) {\n  return (a ?? 0) + b;\n}

Error: tsc reports A required parameter cannot follow an optional parameter. TypeScript enforces this ordering because, at a call site like bad(5), there would be no way to tell whether 5 was meant to fill a or b.

Fix: put required parameters first.

function good(b: number, a?: number): number {\n  return (a ?? 0) + b;\n}\n\nconsole.log(good(5));\nconsole.log(good(5, 10));

Output:

5\n15

Mistake 2: Combining ? with a default value

function greetUser(name?: string = \"friend\"): string {\n  return `Hi, ${name}`;\n}

Error: tsc reports Parameter cannot have question mark and initializer. (error TS1015). A default value already makes the parameter optional, so adding ? on top is not just redundant — TypeScript treats it as a contradiction and refuses to compile.

Fix: drop the ? and keep only the default.

function greetUser(name: string = \"friend\"): string {\n  return `Hi, ${name}`;\n}\n\nconsole.log(greetUser());\nconsole.log(greetUser(\"Sam\"));

Output:

Hi, friend\nHi, Sam

Best Practices

  • Prefer a default value over a bare optional parameter whenever there’s a sensible fallback — it avoids undefined checks scattered through the function body.
  • Reserve optional (?) parameters for cases where \”not provided\” is a meaningfully different state from any real value (e.g. distinguishing \”no callback given\” from \”callback given\”).
  • Keep required parameters first, then optional/default parameters, matching the order the compiler enforces.
  • For functions that take many optional settings, use a single destructured options-object parameter (as in Example 3) instead of a long list of optional parameters — it’s far easier for callers to read at the call site.
  • Remember that explicitly passing undefined triggers a default parameter’s fallback, so fn(undefined) and fn() behave the same for default parameters.
  • Don’t annotate a default parameter’s type unless it differs from what the default value would infer — let inference do the work and keep the signature shorter.

Practice Exercises

  • Write a function formatPrice(amount: number, currency?: string) that returns a string like \"$42\" when currency is omitted (default to \"$\" inside the body) and \"€42\" when called with \"€\". Decide whether ? or a default parameter is the better fit, and justify it.
  • Refactor this signature so it compiles: function logMessage(level: string = \"info\", message: string). What error does tsc give before the fix, and why?
  • Write a function buildUrl(path: string, options: { query?: string; hash?: string } = {}) that returns path alone when options is omitted, and appends ?query and/or #hash when provided. Test it with no options, query only, and both.

Summary

  • ? marks a parameter optional; inside the function its type becomes T | undefined, so you must narrow it before use.
  • A default value (= expr) also makes a parameter optional, but supplies a real fallback — no undefined handling needed, and the parameter keeps type T.
  • A parameter cannot have both ? and a default — TypeScript rejects that as a compile error (TS1015).
  • Required parameters must come before optional and default parameters in the parameter list.
  • Without an explicit annotation, a default parameter’s type is inferred from its default value.
  • All of this is erased at runtime — the compiled JavaScript just checks for undefined and substitutes the default; there’s no way to detect \”was this explicitly passed\” from inside the function.
  • For many optional settings, prefer a single destructured options object with per-property defaults over a long optional-parameter list.