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:
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 3Optional 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:
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 itDefault 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:
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:
// 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 |
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:
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// 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:
// 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// 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 tupleDestructured Parameter Typing
When you destructure an object or array in a parameter list, annotate the parameter as a whole, not the individual bindings:
// 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"{ 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:
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:
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 typedPractical: API Call Function
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
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(); // unsubscribeCommon 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