Skip to main content

How to Use TypeScript Optional Parameters

You're testing a retry function with a custom timeout, so you call retry(apiCall, 0) to retry immediately. The function crashes. Why? Because you wrote if (timeout) instead of if (timeout !== undefined), and TypeScript treated your 0 as a missing argument. This is one of the most frustrating bugs with optional parameters, and it's completely avoidable.

TypeScript optional parameters let you call functions with different numbers of arguments, giving you flexibility without function overloads. But they come with gotchas you need to understand. In this guide, we'll cover how to use optional parameters correctly, avoid common pitfalls, and build more flexible TypeScript functions.

Whether you're building frontend components with React or creating backend functions with Convex, properly handling optional parameters will help you write more robust, maintainable code.

Defining Optional Parameters in TypeScript Functions

In TypeScript, you can make a parameter optional by adding a question mark (?) after its name in the function signature. This indicates the parameter doesn't need to be provided when the function is called.

function greet(name: string, greeting?: string) {
console.log(`${greeting || 'Hello'}, ${name}!`);
}

greet("Alice"); // Output: Hello, Alice!
greet("Bob", "Hi"); // Output: Hi, Bob!

The question mark syntax gives you immediate flexibility in how functions can be called, reducing the need for function overloads or extra parameter checking code.

Syntax for Optional Parameters

The syntax for defining optional parameters is simple: just add a ? after the parameter name.

function logMessage(message?: string) {
console.log(message || "No message provided");
}

logMessage(); // Output: No message provided
logMessage("Hello, world!"); // Output: Hello, world!

When you omit an optional parameter, its value becomes undefined. The code above uses the logical OR operator (||) to provide a fallback value when the parameter is undefined.

Using Optional Parameters with Default Values in TypeScript

Optional parameters can also have default values, which are used when the parameter is not provided. This combines the flexibility of optional parameters with guaranteed values:

function log(message: string, level: string = 'info') {
console.log(`[${level}] ${message}`);
}

log("Server started"); // Output: [info] Server started
log("Database connection failed", "error"); // Output: [error] Database connection failed

Default parameters provide a more explicit way to handle missing values compared to the || fallback pattern. They make your code's intent clearer and can help with argument validation when building robust applications.

Deciding Between Optional Parameters and Default Values

When should you use a default value versus a simple optional parameter? Here's a quick decision guide:

ScenarioUseExample
There's a sensible fallback value used 80%+ of the timeDefault parametertimeout: number = 5000
No reasonable default existsOptional parameteruserId?: string
Behavior changes significantly when omittedOptional parameteroptions?: ConfigObject
You need to distinguish undefined from a valueOptional parameterretryCount?: number (0 vs undefined)
The parameter is rarely omittedRequired parameterurl: string
// Default parameter - most API calls use 5 second timeout
function fetchData(url: string, timeout: number = 5000) {
const controller = new AbortController();
setTimeout(() => controller.abort(), timeout);
return fetch(url, { signal: controller.signal });
}

// Optional parameter - behavior differs when options aren't provided
function processData(data: any[], options?: {sort: boolean, filter: boolean}) {
if (!options) {
return data; // Fast path: no processing
}

let result = [...data];
if (options.sort) result.sort();
if (options.filter) result = result.filter(item => item.active);
return result;
}

// Optional parameter - need to distinguish 0 from undefined
function retry(operation: () => Promise<void>, maxAttempts?: number) {
if (maxAttempts === 0) {
return; // Explicitly disabled retries
}

const attempts = maxAttempts ?? 3; // Default to 3 if undefined
// Retry logic...
}

Key rule: Parameters with default values don't need the ? syntax since they're already optional. Use ? only when the parameter has no default and you want to allow undefined.

When working with default parameter values, TypeScript ensures type safety while providing the flexibility of optional arguments.

Handling Undefined Values with Optional Parameters

Since optional parameters can be undefined, it's important to handle this case properly to avoid runtime errors:

function processUser(user: {name: string, age?: number}) {
console.log(`User: ${user.name}`);

// Safe handling of optional parameter
if (user.age !== undefined) {
console.log(`Age: ${user.age}`);
}
}

Common Pitfalls with TypeScript Optional Parameters

Optional parameters can introduce subtle bugs if you're not careful. Here are the mistakes developers run into most often.

Mistake 1: Using Falsy Checks Instead of Undefined Checks

The most common mistake is checking if (param) instead of if (param !== undefined). This treats falsy values like 0, "", and false as if they weren't provided:

// Wrong: Treats 0 as missing
function retry(operation: () => void, delay?: number) {
if (delay) {
setTimeout(operation, delay);
} else {
operation(); // This runs even when delay is 0!
}
}

retry(saveData, 0); // Expects immediate retry, but gets treated as no delay

// Correct: Explicitly check for undefined
function retry(operation: () => void, delay?: number) {
if (delay !== undefined) {
setTimeout(operation, delay);
} else {
operation();
}
}

retry(saveData, 0); // Now correctly waits 0ms

Use typeof param !== 'undefined' or param !== undefined when you need to distinguish between a missing parameter and a falsy value.

Mistake 2: Combining Optional Syntax with Default Values

You can't use both ? and = on the same parameter. TypeScript will give you a compiler error:

// Error: Parameter cannot have question mark and initializer
function logMessage(message?: string = "No message") {
console.log(message);
}

// Correct: Use default parameter (implicitly optional)
function logMessage(message: string = "No message") {
console.log(message);
}

Parameters with default values are already optional, so the ? is redundant and not allowed.

Mistake 3: Required Parameters After Optional Ones

TypeScript requires all optional parameters to come last. This prevents ambiguity about which arguments map to which parameters:

// Error: A required parameter cannot follow an optional parameter
function createUser(email?: string, name: string) {
// How would TypeScript know if you called createUser("John")?
}

// Correct: Required parameters first
function createUser(name: string, email?: string) {
// Now createUser("John") is unambiguous
}

The one exception is default parameters, which can appear before required parameters if you pass undefined explicitly:

function formatDate(date: Date, format: string = "MM/DD/YYYY", timezone: string) {
// Valid, but confusing
}

formatDate(new Date(), undefined, "UTC"); // Must pass undefined for format

While this works, it's better to keep required parameters first for readability.

Mistake 4: Not Handling Undefined in Object Access

When optional parameters are objects, accessing their properties without checking can cause runtime errors:

// Dangerous: Will crash if options is undefined
function processData(data: string[], options?: { sort: boolean }) {
if (options.sort) { // Error: Cannot read property 'sort' of undefined
data.sort();
}
return data;
}

// Safe: Use optional chaining or explicit checks
function processData(data: string[], options?: { sort: boolean }) {
if (options?.sort) { // Safe with optional chaining
data.sort();
}
return data;
}

Always use optional chaining (?.) or explicit undefined checks when accessing properties of optional parameters.

Handling Optional Parameters in TypeScript Interfaces

Optional parameters can also be used in interfaces to define properties that might not always be present.

interface ApiResponse {
data: string;
error?: string;
}

There are several strategies for handling undefined optional parameters safely:

  1. Use conditional checks:
if (parameter !== undefined) {
// Use parameter safely
}
  1. Use the logical OR operator for default values:
const value = parameter || defaultValue;
  1. Use nullish coalescing for default values (preserves 0 and ''):
const value = parameter ?? defaultValue;
  1. Use optional chaining with optional parameters:
const result = options?.process?.(data);

When working with Convex functions, handling optional parameters correctly helps you build more flexible APIs without compromising type safety.

Type Guards for Optional Parameters

Type guards are particularly useful when working with optional parameters that could have different types. They help TypeScript narrow down the type, giving you full autocomplete and type safety:

function processValue(value?: string | number) {
if (typeof value === 'string') {
return value.toUpperCase(); // TypeScript knows value is string here
} else if (typeof value === 'number') {
return value * 2; // TypeScript knows value is number here
} else {
return null; // TypeScript knows value is undefined here
}
}

This pattern works well with union types and ensures your code remains type-safe even with the flexibility of optional parameters.

Advanced Type Guards with Optional Parameters

For complex optional parameters, use custom type guards or discriminated unions:

interface ApiSuccessResponse {
status: 'success';
data: unknown;
}

interface ApiErrorResponse {
status: 'error';
message: string;
}

type ApiResponse = ApiSuccessResponse | ApiErrorResponse;

// Custom type guard
function isSuccessResponse(response: ApiResponse): response is ApiSuccessResponse {
return response.status === 'success';
}

// Use with optional parameter
function handleApiCall(response?: ApiResponse) {
if (!response) {
console.log('No response received');
return;
}

if (isSuccessResponse(response)) {
console.log('Success:', response.data); // TypeScript knows this is success response
} else {
console.error('Error:', response.message); // TypeScript knows this is error response
}
}

Combining Type Guards with Optional Chaining

You can combine type guards with optional chaining for complex optional parameters:

interface ProcessingOptions {
validators?: Array<(data: unknown) => boolean>;
transformers?: {
beforeSave?: (data: unknown) => unknown;
afterLoad?: (data: unknown) => unknown;
};
}

function processData(data: unknown, options?: ProcessingOptions) {
// Check if validators exist and have length
if (options?.validators && options.validators.length > 0) {
const isValid = options.validators.every(validator => validator(data));
if (!isValid) throw new Error('Validation failed');
}

// Use optional chaining with type guard
const transformed = typeof options?.transformers?.beforeSave === 'function'
? options.transformers.beforeSave(data)
: data;

return transformed;
}

This approach is particularly useful when dealing with deeply nested optional properties in configuration objects.

Checking for the Presence of Optional Parameters

Sometimes you need to know whether an optional parameter was provided, not just handle its potentially undefined value. TypeScript offers several ways to check for parameter presence:

function configureApp(options?: {debug?: boolean, logLevel?: string}) {
// Check if the options object was provided at all
if (options === undefined) {
return defaultConfiguration();
}

// Check if specific options were provided
const debugMode = 'debug' in options ? options.debug : false;
const logLevel = options.logLevel ?? 'info';

// Configure the application
// ...
}

The in operator is useful for checking if a property exists on an object, which differs slightly from checking if its value is undefined. This distinction matters when properties are explicitly set to undefined:

const explicitUndefined = { debug: undefined };
const notProvided = {};

// This is true - property exists but is undefined
'debug' in explicitUndefined;

// This is false - property doesn't exist
'debug' in notProvided;

When building TypeScript applications with Convex, these distinctions help you create flexible APIs that properly handle all edge cases.

Using Object Destructuring with Optional Parameters

Object destructuring provides an elegant way to handle optional parameters, especially when dealing with option objects:

function renderUI({
theme = 'light',
showHeader = true,
user
}: {
theme?: string,
showHeader?: boolean,
user: {name: string, id: string}
}) {
// Implementation using destructured parameters with defaults
}

// Call with only required parameters
renderUI({ user: { name: "Alice", id: "123" } });

// Call with all parameters
renderUI({
theme: 'dark',
showHeader: false,
user: { name: "Bob", id: "456" }
});

This pattern is commonly used in React TypeScript components and other complex interfaces where you need to provide many configuration options.

Managing Multiple Optional Parameters

Functions with multiple optional parameters require special consideration to maintain code clarity and usability:

// Many optional parameters can be unwieldy
function createUser(
name: string,
email?: string,
age?: number,
isAdmin?: boolean,
department?: string,
startDate?: Date
) {
// Implementation
}

// Better approach with options object
function createUser(
name: string,
options?: {
email?: string,
age?: number,
isAdmin?: boolean,
department?: string,
startDate?: Date
}
) {
// Implementation using options.email, options.age, etc.
}

Using an options object has several advantages:

  1. It makes function calls clearer, especially when skipping some optional parameters
  2. It allows for named parameters, reducing errors from parameter order confusion
  3. It makes the function signature more maintainable as requirements change
  4. It works well with interface definitions that can be reused

When building backend functions, this pattern helps you create more maintainable APIs.

Using Optional Parameters in Arrow Functions and Class Methods

Optional parameters work the same way in arrow functions and class methods as they do in regular functions, but there are some patterns worth knowing.

Optional Parameters in Arrow Functions

Arrow functions support optional parameters with the same syntax:

// Arrow function with optional parameter
const formatCurrency = (amount: number, currency?: string): string => {
return `${currency || '$'}${amount.toFixed(2)}`;
};

formatCurrency(42.5); // "$42.50"
formatCurrency(42.5, '€'); // "€42.50"

// With type annotations for callbacks
interface DataFetcher {
fetch: (url: string, options?: RequestInit) => Promise<Response>;
}

const api: DataFetcher = {
fetch: async (url, options) => {
// options is correctly typed as RequestInit | undefined
return fetch(url, options);
}
};

This is particularly useful when defining callbacks or event handlers that might not need all parameters.

Optional Parameters in Class Methods

Class methods can use optional parameters to provide flexible APIs:

class ApiClient {
private baseUrl: string;

constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}

// Regular method with optional parameters
async get(endpoint: string, config?: { headers?: Record<string, string> }) {
const headers = config?.headers || {};
const response = await fetch(`${this.baseUrl}${endpoint}`, { headers });
return response.json();
}

// Arrow function as class property - binds 'this' automatically
post = async (endpoint: string, data?: unknown, config?: { headers?: Record<string, string> }) => {
const headers = config?.headers || { 'Content-Type': 'application/json' };
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers,
body: data ? JSON.stringify(data) : undefined
});
return response.json();
}
}

const client = new ApiClient('https://api.example.com');
client.get('/users');
client.get('/users', { headers: { 'Authorization': 'Bearer token' } });

When to Use Arrow Functions as Class Methods

Arrow functions defined as class properties have an advantage when dealing with optional parameters and the this keyword:

class EventHandler {
private eventCount = 0;

// Regular method - 'this' binding can be lost
handleClick(event?: MouseEvent) {
this.eventCount++; // 'this' might be undefined if not bound
console.log(`Clicks: ${this.eventCount}`);
}

// Arrow function - 'this' is always bound correctly
handleClickSafe = (event?: MouseEvent) => {
this.eventCount++; // 'this' is guaranteed to be the class instance
console.log(`Clicks: ${this.eventCount}`);
};
}

const handler = new EventHandler();

// With regular method, you need to bind
document.addEventListener('click', handler.handleClick.bind(handler));

// With arrow function, no binding needed
document.addEventListener('click', handler.handleClickSafe);

The arrow function approach is especially useful when passing methods as callbacks with optional parameters, since you don't have to worry about losing the this context.

Optional Parameters in Constructor Functions

Constructors can also use optional parameters to provide flexible object initialization:

class User {
name: string;
email: string;
role: string;
createdAt: Date;

constructor(name: string, email: string, role?: string, createdAt?: Date) {
this.name = name;
this.email = email;
this.role = role || 'user';
this.createdAt = createdAt || new Date();
}
}

// Flexible instantiation
const user1 = new User('Alice', 'alice@example.com');
const user2 = new User('Bob', 'bob@example.com', 'admin');
const user3 = new User('Carol', 'carol@example.com', 'moderator', new Date('2024-01-01'));

For constructors with many optional parameters, consider using an options object instead to improve readability.

Optional Parameter Order Restrictions

In TypeScript, all optional parameters must come after required parameters:

// Correct: required parameters first, then optional
function correct(required1: string, required2: number, optional?: boolean) {}

// Error: optional parameters must come after required parameters
function incorrect(required1: string, optional?: boolean, required2: number) {}

This restriction helps TypeScript maintain type safety when calling functions, as it can't determine which argument should map to which parameter if required parameters follow optional ones. The one exception to this rule is when using default values:

// This works because default parameters are effectively optional
function withDefaults(required: string, withDefault: number = 0, alsoRequired: boolean) {
// Implementation
}

withDefaults("test", 42, true); // All parameters provided
withDefaults("test", undefined, true); // Using default for middle parameter

While this is possible, it can lead to confusing code. For better readability, keep required parameters first, followed by parameters with defaults, and finally truly optional parameters.

Documenting Optional Parameters for Code Clarity

Clear documentation is essential for functions with optional parameters, especially in team environments or public APIs:

/**
* Fetches user data from the API
* @param userId - The unique identifier of the user
* @param options - Optional settings
* @param options.includeActivity - Whether to include user activity data
* @param options.sessionToken - Authentication token (required if user is requesting own data)
* @returns User data object
*/
function fetchUser(
userId: string,
options?: {
includeActivity?: boolean,
sessionToken?: string
}
): Promise<User> {
// Implementation
}

Good documentation clarifies:

  • Which parameters are optional
  • What happens when optional parameters are omitted
  • Any interdependencies between parameters
  • Default values used for optional parameters

When working with TypeScript in larger projects, this documentation becomes increasingly valuable. Tools like TSDoc help generate API documentation automatically from your comments.

Using JSDoc for Function Documentation

JSDoc comments provide a standard way to document TypeScript functions:

/**
* Processes payment for an order
*
* @param orderId - Unique order identifier
* @param amount - Payment amount
* @param {Object} [options] - Optional configuration (square brackets indicate optional)
* @param {string} [options.currency="USD"] - Currency code (defaults to USD)
* @param {boolean} [options.sendReceipt=true] - Whether to send email receipt
* @returns {Promise<PaymentResult>} Result of payment processing
*
* @example
* // Process payment with default options
* processPayment("order-123", 99.99);
*
* // Process payment with custom options
* processPayment("order-456", 149.99, { currency: "EUR", sendReceipt: false });
*/
function processPayment(
orderId: string,
amount: number,
options?: {
currency?: string,
sendReceipt?: boolean
}
): Promise<PaymentResult> {
// Implementation with sensible defaults
const currency = options?.currency || "USD";
const sendReceipt = options?.sendReceipt !== false;

// Process payment
// ...
}

Good documentation helps maintain code quality as your project grows.

Testing Functions with Optional Parameters in TypeScript

Test functions with optional parameters by creating test cases for each parameter combination:

describe('greet function', () => {
it('works with only required parameters', () => {
const result = greet("Alice");
expect(result).toBe("Hello, Alice!");
});

it('works with optional parameters provided', () => {
const result = greet("Bob", "Hi");
expect(result).toBe("Hi, Bob!");
});
});

Include test cases that verify default behaviors when optional parameters are omitted and edge cases with unusual values.

Converting Existing JavaScript Functions to Use TypeScript Optional Parameters

When migrating from JavaScript to TypeScript, identify parameters that aren't always required:

// JavaScript version
function sendNotification(user, message, channel) {
channel = channel || 'email';
// Implementation
}

// TypeScript version
function sendNotification(user: User, message: string, channel?: 'email' | 'sms' | 'push') {
const selectedChannel = channel || 'email';
// Implementation with type safety
}

Look for parameters that use default values, optional chaining, or conditional logic - these are natural candidates for optional parameters in TypeScript.

Rules of Thumb for TypeScript Optional Parameters

TypeScript optional parameters give you flexibility without sacrificing type safety, but you need to use them correctly. Here's a decision framework to guide you:

Quick Decision Guide

When checking optional parameters:

  • ✅ Use param !== undefined or typeof param !== 'undefined' for explicit checks
  • ❌ Don't use if (param) if 0, "", or false are valid values

When choosing between approaches:

  • Use default parameters when 80%+ of calls use the same value
  • Use optional parameters when there's no sensible default
  • Use an options object when you have 3+ optional parameters

When defining functions:

  • Required parameters come first, optional parameters last
  • Don't combine ? and = on the same parameter (use just = for defaults)
  • Use optional chaining (?.) when accessing properties of optional objects

When working with types:

  • Use type guards to narrow string | number | undefined to specific types
  • Consider custom type guards for complex optional parameters
  • Use discriminated unions when optional parameters have different shapes

Common Patterns to Avoid

// ❌ Avoid: Falsy check treats 0 as undefined
if (timeout) { }

// ✅ Better: Explicit undefined check
if (timeout !== undefined) { }

// ❌ Avoid: Too many optional parameters
function create(a: string, b?: number, c?: boolean, d?: string, e?: number) { }

// ✅ Better: Options object
function create(a: string, options?: { b?: number, c?: boolean, d?: string, e?: number }) { }

// ❌ Avoid: Combining ? and =
function log(msg?: string = "default") { }

// ✅ Better: Just use default parameter
function log(msg: string = "default") { }

When Optional Parameters Aren't the Answer

If your function's behavior changes drastically based on which parameters are provided, you might need:

  • Function overloads for completely different signatures
  • Separate functions when the logic diverges significantly
  • Required configuration objects when parameters aren't truly optional

Optional parameters work best when they add convenience without changing the fundamental behavior of your function. Use them to make your TypeScript APIs more flexible and developer-friendly.