Optional and Default Parameters

7 min read

Real test helpers rarely need every argument every time. A waitForElement helper has a sensible default timeout. A createUser helper accepts a role but assumes "tester" if you don't pass one. A logSteps helper takes any number of step descriptions. TypeScript has three tools for these patterns — optional, default, and rest parameters — each with rules about ordering and behaviour. This lesson covers all three.

Optional parameters — ?

A ? after a parameter name marks it as optional. The caller may omit it; inside the function the parameter has type T | undefined.

interface User {
  name: string;
  email: string;
  role: string;
}
 
function createUser(name: string, email: string, role?: string): User {
  return { name, email, role: role ?? "tester" };
}
 
createUser("Alice", "alice@test.com");                 // ✅ role defaults to "tester"
createUser("Bob", "bob@test.com", "admin");            // ✅ role is "admin"
createUser("Carol", "carol@test.com", undefined);      // ✅ explicit undefined

The ?? operator is nullish coalescing — "use the left side if it's not null or undefined, otherwise the right side." It's the cleanest way to apply a fallback without the truthiness pitfalls of ||.

Rule: optional parameters must come after required ones. The compiler refuses the reverse:

function bad(role?: string, name: string) { return { name, role }; }
// ❌ A required parameter cannot follow an optional parameter.

If you need flexibility around the order, group the flexible bits into an options object — see the runner example below.

Default parameters

Provide a value with = and the parameter takes that value when the caller omits it. Default-valued parameters are automatically optional at the call site.

function waitForElement(selector: string, timeout: number = 5000): void {
  console.log(`Waiting up to ${timeout}ms for ${selector}`);
}
 
waitForElement(".login-btn");          // timeout = 5000
waitForElement(".login-btn", 10000);   // timeout = 10000
waitForElement(".login-btn", undefined); // timeout = 5000 (default kicks in for undefined)

Inside the function, timeout has type number — not number | undefined. The default removes the "could be missing" branch entirely. That's the practical reason to prefer default over optional whenever you have a sensible fallback: cleaner type, no narrowing, simpler code.

// Optional — body has to handle undefined
function a(timeout?: number): void {
  // timeout is number | undefined here
}
 
// Default — body never sees undefined
function b(timeout: number = 5000): void {
  // timeout is just number here
}

Reach for ? only when the absence of the parameter has its own meaning beyond "use the default."

Rest parameters — ...args

A rest parameter collects any number of trailing arguments into an array. The annotation is the array type:

function logSteps(...steps: string[]): void {
  steps.forEach((step, i) => console.log(`Step ${i + 1}: ${step}`));
}
 
logSteps("Navigate to login", "Enter username", "Click submit");
// Step 1: Navigate to login
// Step 2: Enter username
// Step 3: Click submit

The ... syntax is the JavaScript spread/rest you already know — TypeScript adds the : string[] annotation. Rules:

  • A function may have at most one rest parameter.
  • It must come last in the parameter list.
  • Inside the function, the rest parameter is a real array — length, forEach, map all work.

Rest parameters are how variadic test runners, log builders, and assertion chains accept "as many things as you want to pass."

An options-object pattern for QA helpers

Once a function has more than two or three flexible inputs, individual ? parameters get unwieldy — every call site has to remember the order. The everyday solution: take a single options object with each field defaulted via destructuring.

function runTest(
  testName: string,
  options: {
    browser?: "chromium" | "firefox" | "webkit";
    headless?: boolean;
    retries?: number;
  } = {}
): void {
  const { browser = "chromium", headless = true, retries = 0 } = options;
  console.log(`Running ${testName} on ${browser}, headless: ${headless}, retries: ${retries}`);
}
 
runTest("Login");
runTest("Login", { browser: "firefox" });
runTest("Login", { browser: "webkit", headless: false, retries: 2 });

The whole options parameter has a default of {}, so callers can omit it entirely. Inside, destructuring with = provides per-field defaults. This is the pattern Cypress, Playwright, and most modern test libraries use for configuration — once you recognise it, you'll see it everywhere.

Three flavours of "may be missing"

(Each bar is the same height — this is a labelled cheat-sheet of the four parameter modifiers, plus the options-object pattern that combines them.)

A QA-shaped helper using all of them

A configurable test runner that exercises every tool from this lesson:

function runSuite(
  suiteName: string,                           // required
  baseUrl: string = "https://staging.app.com", // default
  options: {
    browser?: "chromium" | "firefox" | "webkit";
    headless?: boolean;
    retries?: number;
  } = {},                                      // default = {}
  ...tags: string[]                            // rest
): void {
  const { browser = "chromium", headless = true, retries = 0 } = options;
 
  console.log(`Suite: ${suiteName}`);
  console.log(`URL:   ${baseUrl}`);
  console.log(`Run:   ${browser} (headless=${headless}, retries=${retries})`);
  if (tags.length > 0) console.log(`Tags:  ${tags.join(", ")}`);
}
 
runSuite("Smoke");
runSuite("Smoke", "https://qa.app.com");
runSuite("Smoke", "https://qa.app.com", { browser: "firefox", retries: 2 });
runSuite("Smoke", "https://qa.app.com", {}, "auth", "critical-path");

The signature is the documentation. A reader can tell from the annotation alone what's required, what's default, what the options shape looks like, and that any trailing strings are tags.

⚠️ Common mistakes

  • Using optional when default would do. function wait(timeout?: number) { const t = timeout ?? 5000; ... } works but spreads the default across the body. function wait(timeout: number = 5000) keeps it at the signature, where the contract belongs. Prefer default whenever a sensible fallback exists.
  • Putting an optional parameter before a required one. function f(x?: number, y: string) is a compile error. The fix is either to reorder (function f(y: string, x?: number)) or to switch to an options-object pattern when ordering is awkward.
  • Reaching for rest parameters when an array would be clearer. logSteps("a", "b", "c") is fine. logSteps(...steps) from a caller that already has an array becomes noisy. If the caller almost always passes an existing array, declare the parameter as steps: string[] and let them pass it directly.

🎯 Practice task

Build a configurable test helper. 20-30 minutes.

  1. In your ts-for-qa/src folder, create runner-config.ts.
  2. Define interface User { name: string; email: string; role: "admin" | "tester" | "viewer" }.
  3. Write function createUser(name: string, email: string, role: User["role"] = "tester"): User using a default parameter (no optional ?).
  4. Write function logSteps(prefix: string, ...steps: string[]): void that prints each step prefixed with prefix:.
  5. Write function runSuite(name: string, options: { browser?: "chromium" | "firefox" | "webkit"; headless?: boolean; retries?: number } = {}): void using the options-object pattern with destructuring defaults.
  6. Call each function with and without the optional inputs. Run with npx ts-node src/runner-config.ts.
  7. Trigger the ordering rules. Try function bad(role?: string, name: string) — read the error and revert.
  8. Stretch: convert one of the optional parameters to a default and observe how the body simplifies — no ?? fallback inside, the type is no longer ... | undefined. Note in a comment which trade-off (optional vs default) you'd pick for that helper in a real test suite, and why.

The next lesson covers function overloads — describing one function whose return type depends on the argument type, the way Cypress and Playwright type their multi-shape APIs.

// tip to track lessons you complete and pick up where you left off across devices.