How to Set & Use TypeScript Default Parameters
You're calling an API client function, but half the time you forget to pass the timeout value, so you end up with repetitive code like timeout = timeout || 30000 scattered everywhere. Default parameters solve this by letting you specify fallback values right in the function signature, eliminating conditional checks and making your code cleaner.
In this guide, you'll learn how to use default parameters effectively in TypeScript, including how they interact with optional parameters, how to handle object destructuring, and the gotchas you need to watch for when working with null and undefined.
Setting Default Parameters in TypeScript Functions
To assign a default value to a parameter in a TypeScript function, use the syntax parameterName: type = defaultValue. Here's a realistic example:
function fetchWithTimeout(url: string, timeout: number = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
return fetch(url, { signal: controller.signal })
.finally(() => clearTimeout(timeoutId));
}
fetchWithTimeout('/api/users'); // Uses 5000ms timeout
fetchWithTimeout('/api/users', 10000); // Uses 10000ms timeout
This lets you specify fallback values for function parameters without cluttering your code with conditional logic. When working with TypeScript function types, default parameters are especially useful for handling common cases without extra code.
Default parameters in Convex can similarly help when defining backend functions by providing sensible defaults that improve both clarity and flexibility.
Handling Optional Parameters with Default Values
You can combine optional parameters with default values to create versatile functions. Here's the key difference: optional parameters (marked with ?) become undefined when omitted, while default parameters provide an actual fallback value.
function queryDatabase(
tableName: string,
limit: number = 100,
sortBy?: string,
cacheResults: boolean = true
) {
const query = {
table: tableName,
limit,
sort: sortBy, // Will be undefined if not provided
useCache: cacheResults
};
return executeQuery(query);
}
// Uses defaults for limit and cacheResults, sortBy is undefined
queryDatabase('users');
// Override defaults as needed
queryDatabase('products', 50, 'price', false);
Using default values for optional parameters lets you create functions that are both flexible and straightforward. Notice how sortBy remains optional without a default (it's genuinely optional), while limit and cacheResults have sensible fallbacks.
This pattern works well when building custom functions in Convex backends, where you often need to handle various input combinations while maintaining type safety.
Defining Functions with Default Arguments
Default parameters work great when you want functions that can be called with varying numbers of arguments. Here's a practical example with an HTTP client:
function makeRequest(
endpoint: string,
method: string = 'GET',
headers: Record<string, string> = {},
retries: number = 3
) {
// Retry logic with exponential backoff
let attemptCount = 0;
const attemptFetch = async (): Promise<Response> => {
try {
return await fetch(endpoint, { method, headers });
} catch (error) {
if (attemptCount < retries) {
attemptCount++;
await new Promise(resolve => setTimeout(resolve, 1000 * attemptCount));
return attemptFetch();
}
throw error;
}
};
return attemptFetch();
}
// All defaults
makeRequest('/api/users');
// Override method only
makeRequest('/api/users', 'POST');
// Full control when needed
makeRequest('/api/users', 'DELETE', { 'Authorization': 'Bearer token' }, 5);
When working with arrow functions in TypeScript, you can apply the same default parameter pattern:
const calculateDiscount = (price: number, discountRate: number = 0.1) =>
price * (1 - discountRate);
For building full-stack TypeScript apps, these patterns help create well-typed backend functions that gracefully handle missing parameters.
Ensuring Backward Compatibility with Default Parameters
When you're evolving an API or library, default parameters let you add new functionality without breaking existing callers. Here's a real-world scenario:
// Version 1.0 - original function
function sendAnalyticsEvent(eventName: string, userId: string) {
return fetch('/analytics', {
method: 'POST',
body: JSON.stringify({ eventName, userId })
});
}
// Version 2.0 - added batching support without breaking v1.0 callers
function sendAnalyticsEvent(
eventName: string,
userId: string,
enableBatching: boolean = false,
batchSize: number = 10
) {
if (enableBatching) {
// New batching logic
return addToBatch({ eventName, userId }, batchSize);
}
// Original behavior preserved
return fetch('/analytics', {
method: 'POST',
body: JSON.stringify({ eventName, userId })
});
}
// Old code still works
sendAnalyticsEvent('page_view', 'user-123');
// New code can opt into batching
sendAnalyticsEvent('page_view', 'user-123', true, 20);
Default parameters let you enhance existing functions without disrupting previous code. When working with TypeScript interfaces or in larger codebases, this becomes crucial for smooth version upgrades.
This approach is similar to how Convex handles schema evolution, where backward compatibility is maintained while adding new fields and capabilities.
Using Default Parameter Values Effectively
Here are the rules I follow when deciding whether to use default parameters:
- Use default parameters for optional configuration or behavior flags
- Avoid defaults for critical business values (like prices or user IDs)
- Keep default values simple - avoid complex computations or function calls
- Make defaults represent the most common use case
Here's an example that shows good default parameter usage:
function createLogger(
namespace: string,
logLevel: 'debug' | 'info' | 'warn' | 'error' = 'info',
enableConsole: boolean = true,
enableFileOutput: boolean = false
) {
return {
log: (message: string, level: typeof logLevel = logLevel) => {
const timestamp = new Date().toISOString();
const formatted = `[${timestamp}] [${namespace}] [${level}] ${message}`;
if (enableConsole) {
console.log(formatted);
}
if (enableFileOutput) {
writeToFile(formatted);
}
}
};
}
// Minimal config for local development
const logger = createLogger('api');
// Production setup with file output
const prodLogger = createLogger('api', 'warn', true, true);
When working with function return types in TypeScript, default parameters can make your APIs more intuitive while preserving type safety.
The Convex types cookbook offers similar patterns for creating flexible, type-safe backend functions that handle various input combinations gracefully.
Default Parameters with Object Destructuring
One of the most powerful patterns combines default parameters with object destructuring. This gives you named parameters and default values in one shot:
type RequestConfig = {
method?: string;
headers?: Record<string, string>;
timeout?: number;
retries?: number;
};
function apiCall(
endpoint: string,
{
method = 'GET',
headers = {},
timeout = 5000,
retries = 3
}: RequestConfig = {}
) {
// Now you have all defaults and can call with any combination
return fetch(endpoint, {
method,
headers,
signal: AbortSignal.timeout(timeout)
});
}
// All defaults
apiCall('/api/users');
// Named parameters - order doesn't matter
apiCall('/api/users', {
timeout: 10000,
method: 'POST'
});
// Can omit any combination
apiCall('/api/products', { retries: 5 });
Notice the = {} after the destructured parameter. This makes the entire config object optional, so you can call apiCall('/api/users') without any config at all.
This pattern works well with TypeScript object types, allowing you to create flexible APIs while maintaining type safety. The default values make your functions easier to use while reducing repetitive code.
Similar patterns help create interfaces that are both powerful and developer-friendly when building with Convex.
Applying Default Parameters for Cleaner Code
Default parameters eliminate the need for defensive checks at the start of your functions. Here's a before and after comparison:
// Without default parameters - lots of boilerplate
function fetchData(endpoint: string, method?: string, timeout?: number, retry?: boolean) {
// All this just to handle defaults
const actualMethod = method || 'GET';
const actualTimeout = timeout !== undefined ? timeout : 30000;
const shouldRetry = retry !== undefined ? retry : true;
// Actual implementation...
return makeHttpRequest(endpoint, actualMethod, actualTimeout, shouldRetry);
}
// With default parameters - much cleaner!
function fetchData(
endpoint: string,
method: string = 'GET',
timeout: number = 30000,
retry: boolean = true
) {
// Go straight to the implementation
return makeHttpRequest(endpoint, method, timeout, retry);
}
Notice how the second version cuts out all the defensive parameter normalization. You can trust that method, timeout, and retry have valid values without any checks.
This approach works well with TypeScript question mark operators and the nullish coalescing operator, which serve similar purposes but in different contexts.
For Convex developers, applying default parameters follows similar patterns to those described in the Convex documentation, making your backend code more robust and maintainable.
Understanding Parameter Order with Defaults
TypeScript lets you place default parameters anywhere in your parameter list, but it's not always practical. Here's what you need to know:
// Recommended: defaults after required parameters
function buildUrl(host: string, path: string, port: number = 443) {
return `https://${host}:${port}${path}`;
}
buildUrl('api.example.com', '/users'); // Works naturally
// Technically valid but awkward: default before required parameter
function buildUrl(host: string, port: number = 443, path: string) {
return `https://${host}:${port}${path}`;
}
// Now you must explicitly pass undefined to use the default
buildUrl('api.example.com', undefined, '/users'); // Awkward!
Here's the rule: put default parameters at the end unless you have a good reason not to. If a default parameter comes before a required one, callers have to pass undefined explicitly to trigger the default, which defeats the purpose of having defaults in the first place.
When Default Parameters Don't Work: The null Gotcha
Here's a common mistake: default parameters activate for undefined, but not for null. This catches developers off guard:
function greetUser(name: string = 'Guest') {
console.log(`Hello, ${name}!`);
}
greetUser(); // "Hello, Guest!" - omitted argument becomes undefined
greetUser(undefined); // "Hello, Guest!" - explicit undefined works
greetUser(null); // "Hello, null!" - null does NOT trigger the default!
Why does this happen? In JavaScript (and therefore TypeScript), null is a deliberate value meaning "intentionally empty," while undefined means "not provided." Default parameters only activate when the value is literally undefined.
Here's how to handle this in practice:
function processData(
data: string | null,
encoding: string = 'utf-8'
) {
// If null is a valid input, handle it explicitly
if (data === null) {
return null;
}
return Buffer.from(data, encoding);
}
// Or use nullish coalescing if you want null to also use the default
function processData(
data: string | null,
encoding: string = 'utf-8'
) {
const actualData = data ?? 'default value';
return Buffer.from(actualData, encoding);
}
This pattern is particularly useful when working with TypeScript function types in library code or public APIs.
The Convex custom functions approach uses similar patterns to maintain compatibility while evolving backend APIs.
Key Takeaways
Default parameters clean up your code by eliminating defensive null checks and making your function signatures self-documenting. Use them for configuration options and behavior flags, but avoid them for critical business data that should be explicitly provided.
Remember these rules:
- Default parameters trigger on
undefined, notnull - Put defaults at the end of your parameter list for better ergonomics
- Combine destructuring with defaults for flexible, named-parameter-style APIs
- Use defaults to maintain backward compatibility when evolving APIs
When paired with TypeScript's type system, default parameters create intuitive interfaces that work well in both small projects and large codebases. They're especially valuable when working with TypeScript interfaces, allowing you to evolve your APIs without breaking existing code.