TypeScriptParameters (optional, default, rest)

Parameters

TypeScript gives you precise control over every parameter a function accepts: required, optional, defaulted, rest, destructured, and more. Getting parameters right is one of the highest-leverage TypeScript skills — a well-typed parameter list documents intent, prevents misuse, and narrows types automatically inside the function body.

Required Parameters

By default, every parameter is required. TypeScript errors if you pass too few or too many arguments:

TS
function greet(first: string, last: string): string {
  return `Hello, ${first} ${last}`;
}

greet('Alice', 'Smith');         // OK
// greet('Alice');               // TS2554 — Expected 2 arguments, got 1
// greet('Alice', 'Smith', '!'); // TS2554 — Expected 2 arguments, got 3
Note
Required parameters must always come before optional parameters. TypeScript rejects a required parameter after an optional one.
Optional Parameters with ?

Add ? after the parameter name to make it optional. Inside the function body, the parameter type becomes T | undefined, so you must handle the absent case:

TS
function greet(first: string, last?: string): string {
  if (last !== undefined) {
    return `Hello, ${first} ${last}`;
  }
  return `Hello, ${first}`;
}

greet('Alice');           // "Hello, Alice"
greet('Alice', 'Smith');  // "Hello, Alice Smith"

// Inside the function, last is string | undefined
// TypeScript forces you to narrow before using it
Default Parameter Values

Assign a default value with = to make a parameter optional AND give it a sensible fallback. You do not need a type annotation — TypeScript infers it from the default:

TS
function repeat(text: string, times = 3): string {
  return text.repeat(times);
}

repeat('ha');     // "hahaha"    — times defaults to 3
repeat('ha', 5);  // "hahahahaha"
repeat('ha', 1);  // "ha"

// TypeScript infers times: number from the default value 3
// The parameter is optional at the call site — you can omit it
// But inside the function, times is always number (never undefined)
Optional ? vs Default Value

These two look similar but behave differently inside the function body:

TS
// Optional parameter — inside, count is number | undefined
function logA(count?: number): void {
  console.log(count);         // number | undefined
  console.log(count ?? 0);    // you must provide the fallback yourself
}

// Default value — inside, count is always number
function logB(count = 0): void {
  console.log(count);         // number — never undefined
  console.log(count + 1);     // safe — no narrowing needed
}

// You can still pass undefined explicitly to trigger the default:
logB(undefined); // count = 0  (undefined triggers the default)

Feature

Inside type

Caller can omit?

Caller can pass undefined?

Required

T

No

No

Optional (param?: T)

T | undefined

Yes

Yes

Default (param = value)

T

Yes

Yes — triggers default

Tip
Prefer default values over optional parameters whenever there is a sensible fallback. The function body stays cleaner because the type is never T | undefined.
Rest Parameters

A rest parameter collects all remaining arguments into an array. It must be the last parameter and is typed as an array:

TS
function sum(first: number, ...rest: number[]): number {
  return rest.reduce((acc, n) => acc + n, first);
}

sum(1);              // 1
sum(1, 2, 3);        // 6
sum(1, 2, 3, 4, 5);  // 15

// rest is always number[] — never undefined
// TypeScript infers it from the spread at the call site

TS
// A tagged template helper
function tag(strings: TemplateStringsArray, ...values: unknown[]): string {
  return strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '');
}

const name = 'world';
const result = tag`Hello ${name}!`;
console.log(result); // "Hello world!"
Rest Parameters with Tuple Types

Rest parameters can also be typed as a tuple when you need a fixed set of extra arguments with heterogeneous types:

TS
// Exactly two extra args: a string and a number
function log(level: string, ...args: [string, number]): void {
  const [message, code] = args;
  console.log(`[${level}] ${message} (code ${code})`);
}

log('ERROR', 'Not found', 404);
log('INFO',  'Created',   201);
// log('WARN', 'Bad request'); // TS error — missing second element

TS
// Spread a tuple as arguments — the reverse direction
type RGB = [number, number, number];

function makeColor(r: number, g: number, b: number): string {
  return `rgb(${r}, ${g}, ${b})`;
}

const red: RGB = [255, 0, 0];
makeColor(...red); // TypeScript knows this is safe — RGB is a tuple
Destructured Parameter Typing

When you destructure an object or array in a parameter list, annotate the parameter as a whole, not the individual bindings:

TS
// Annotate the whole parameter
function renderUser({ name, age }: { name: string; age: number }): string {
  return `${name} (age ${age})`;
}

// Better: name the type
type User = { name: string; age: number; role?: string };

function renderUser2({ name, age, role = 'guest' }: User): string {
  return `[${role}] ${name}, ${age}`;
}

renderUser2({ name: 'Alice', age: 30 });           // "[guest] Alice, 30"
renderUser2({ name: 'Bob', age: 25, role: 'admin' }); // "[admin] Bob, 25"
Warning
Do not annotate individual destructured bindings — TypeScript does not support{ name: string, age: number } with colons on each binding. The colon in destructuring means "rename to", not "type as". Always annotate the whole parameter.
Destructuring with Defaults

You can combine destructuring, renaming, and defaults in one parameter:

TS
type Config = {
  host?: string;
  port?: number;
  ssl?: boolean;
};

function connect({
  host = 'localhost',
  port = 5432,
  ssl  = false,
}: Config = {}): string {
  const protocol = ssl ? 'sslmode=require' : 'sslmode=disable';
  return `postgresql://${host}:${port}?${protocol}`;
}

connect();                                // "postgresql://localhost:5432?sslmode=disable"
connect({ port: 5433, ssl: true });       // "postgresql://localhost:5433?sslmode=require"
connect({ host: 'db.example.com' });      // "postgresql://db.example.com:5432?sslmode=disable"
The Parameters Utility Type

Parameters<T> extracts the parameter types of a function type as a tuple. This is useful when you want to wrap or proxy an existing function:

TS
function fetchUser(id: string, options: { timeout: number }): Promise<unknown> {
  return fetch(`/users/${id}`, { signal: AbortSignal.timeout(options.timeout) });
}

// Extract the parameter tuple
type FetchUserArgs = Parameters<typeof fetchUser>;
// → [id: string, options: { timeout: number }]

// Use it to type a wrapper
function withRetry(fn: typeof fetchUser, retries: number) {
  return async (...args: Parameters<typeof fetchUser>) => {
    for (let i = 0; i <= retries; i++) {
      try {
        return await fn(...args);
      } catch (err) {
        if (i === retries) throw err;
      }
    }
  };
}

const resilientFetch = withRetry(fetchUser, 3);
resilientFetch('user-123', { timeout: 5000 }); // fully typed
Practical: API Call Function

TS
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';

interface RequestOptions {
  method?: HttpMethod;
  headers?: Record<string, string>;
  body?: unknown;
  timeout?: number;
}

async function apiCall<T>(
  endpoint: string,
  {
    method  = 'GET',
    headers = {},
    body,
    timeout = 10_000,
  }: RequestOptions = {}
): Promise<T> {
  const res = await fetch(`https://api.example.com${endpoint}`, {
    method,
    headers: { 'Content-Type': 'application/json', ...headers },
    body: body !== undefined ? JSON.stringify(body) : undefined,
    signal: AbortSignal.timeout(timeout),
  });

  if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
  return res.json() as Promise<T>;
}

// Usage
interface Post { id: number; title: string }

const post = await apiCall<Post>('/posts/1');
const created = await apiCall<Post>('/posts', {
  method: 'POST',
  body: { title: 'Hello' },
});
Practical: Typed Event System

TS
type EventMap = Record<string, unknown[]>;

class EventBus<TMap extends EventMap> {
  private listeners = new Map<string, Array<(...args: unknown[]) => void>>();

  on<K extends keyof TMap & string>(
    event: K,
    listener: (...args: TMap[K]) => void
  ): () => void {
    const list = this.listeners.get(event) ?? [];
    list.push(listener as (...args: unknown[]) => void);
    this.listeners.set(event, list);
    return () => this.off(event, listener);
  }

  off<K extends keyof TMap & string>(
    event: K,
    listener: (...args: TMap[K]) => void
  ): void {
    const list = this.listeners.get(event) ?? [];
    this.listeners.set(
      event,
      list.filter((l) => l !== listener)
    );
  }

  emit<K extends keyof TMap & string>(event: K, ...args: TMap[K]): void {
    (this.listeners.get(event) ?? []).forEach((l) => l(...args));
  }
}

type AppEvents = {
  'user:login':  [userId: string, timestamp: Date];
  'user:logout': [userId: string];
  'message':     [from: string, text: string];
};

const bus = new EventBus<AppEvents>();

const off = bus.on('user:login', (userId, timestamp) => {
  console.log(`${userId} logged in at ${timestamp.toISOString()}`);
});

bus.emit('user:login', 'u-1', new Date());
off(); // unsubscribe
Success
Typed rest parameters combined with tuple types give you a completely type-safe event system. TypeScript knows exactly how many arguments each event carries and what types they are — no casting required.
Common Mistakes
  • Putting required params after optional ones — TypeScript rejects this

  • Annotating individual destructured bindings with colons — use { name }: { name: string } not { name: string }

  • Forgetting that optional params are T | undefined inside the body — always narrow

  • Using rest params mid-list — rest must always be the last parameter

  • Overusing optional params when a default value is cleaner and safer

Quick Reference
  • Required: param: T — must be provided, no default

  • Optional: param?: T — T | undefined inside the function body

  • Default: param = value — always T inside the body; caller can omit

  • Rest: ...args: T[] — collects remaining arguments into an array

  • Rest tuple: ...args: [T, U] — fixed heterogeneous rest

  • Destructured: ({ a, b }: MyType) — annotate the whole parameter

  • Parameters<typeof fn> — extracts parameter types as a tuple