When I write TypeScript, I use default parameters all the time to make functions easier to call and less error‑prone. In this guide, I’ll walk through how default parameters work, where they shine, some common pitfalls, and a few patterns I use in real projects.
What are the default parameters in TypeScript?
A default parameter is just a function parameter that has a fallback value.
If the caller:
- does not pass that argument at all, or
- passes
undefinedexplicitly
Then TypeScript (and JavaScript) uses the default value you defined.
This is handy when you want a parameter to be optional for the caller, but you still want a safe value at runtime instead of undefined.
Basic syntax and a simple example
Here’s the most basic form:
function greet(name: string, greeting: string = "Hello"): string {
return `${greeting}, ${name}!`;
}
console.log(greet("Alice")); // Hello, Alice!
console.log(greet("Bob", "Hi")); // Hi, Bob!
console.log(greet("Sam", undefined)); // Hello, Sam!
A few things to notice:
greetinghas a default value “Hello”.- When I call greet(“Alice”), the default is used.
- When I call greet(“Bob”, “Hi”), the passed value overrides the default.
- Passing undefined also triggers the default.
This behavior (default applies on undefined) is important and we’ll come back to it.
Default parameters with multiple arguments
You can use defaults on any number of parameters, not just the last one.
Realistically, you’ll often see this around functions that wrap APIs or configuration options. For example, a simple pagination helper:
function fetchUsers(
page: number = 1,
pageSize: number = 20,
includeInactive: boolean = false
): void {
console.log(
`Fetching page ${page} with size ${pageSize}, includeInactive=${includeInactive}`
);
}
fetchUsers(); // page 1, size 20, includeInactive=false
fetchUsers(2); // page 2, size 20, includeInactive=false
fetchUsers(3, 50, true); // page 3, size 50, includeInactive=true
You can refer to the screenshot below to see the output.

Why this is nice:
- Callers get sensible defaults most of the time: fetchUsers() is enough.
- If a page needs a different page size or needs inactive users, they can override the defaults.
Another example you’ll commonly see in apps:
function createUser(
firstName: string,
lastName: string,
age: number = 30,
country: string = "USA"
): string {
return `Name: ${firstName} ${lastName}, Age: ${age}, Country: ${country}`;
}
console.log(createUser("John", "Doe"));
// Name: John Doe, Age: 30, Country: USA
console.log(createUser("Jane", "Smith", 25));
// Name: Jane Smith, Age: 25, Country: USA
console.log(createUser("Emily", "Johnson", 40, "Canada"));
// Name: Emily Johnson, Age: 40, Country: Canada
You can refer to the screenshot below to see the output.

Defaults keep the function signature readable and avoid a bunch of if checks inside the function body.
Default parameters vs optional parameters (?)
This is a common confusion: default parameters and “optional” parameters are not the same thing.
Optional parameter (?)
With an optional parameter, the caller can omit the argument, but there is no automatic runtime default.
function logMessage(message: string, prefix?: string): void {
console.log(prefix + message);
}
logMessage("System started"); // "undefinedSystem started"
Here:
- prefix is typed as string | undefined.
- If you call logMessage(“System started”), prefix is undefined.
- TypeScript lets this compile, but at runtime you get “undefinedSystem started” unless you add your own checks.
Realistically, you’d write:
function logMessageSafe(message: string, prefix?: string): void {
const actualPrefix = prefix ?? "[INFO] ";
console.log(actualPrefix + message);
}
Default parameter
With a default parameter, you get a real runtime default:
function logMessageDefault(
message: string,
prefix: string = "[INFO] "
): void {
console.log(prefix + message);
}
logMessageDefault("System started"); // [INFO] System started
logMessageDefault("Disk full", "[WARN] "); // [WARN] Disk full
You can refer to the screenshot below to see the output.

Here:
- prefix is always a string at runtime.
- You don’t need extra checks inside the function.
Rule of thumb I use:
- If
undefinedis a meaningful value, I use an optional parameter (?). - If I always want a usable value, I use a default parameter.
How default parameters behave with undefined and null
One subtle but important detail: the default only kicks in when the argument is undefined or omitted.
function toUpper(text: string = "default"): void {
console.log(text.toUpperCase());
}
toUpper(); // DEFAULT
toUpper(undefined); // DEFAULT
// @ts-expect-error
toUpper(null); // runtime error
- No argument → uses “default”.
- undefined → uses “default”.
- null → no default, and at runtime you’ll get an error because null.toUpperCase() is invalid.
So if there’s a chance null might appear, you still need to handle it explicitly.
Use default parameters with destructured objects
For anything with more than a couple of options, I prefer a single “options” object with defaults in the parameter list. It keeps the call site readable and the function flexible.
Let’s say we have a small HTTP wrapper:
interface RequestConfig {
url: string;
method?: string;
timeout?: number;
}
function fetchData({
url,
method = "GET",
timeout = 5000,
}: RequestConfig): void {
console.log(
`Fetching data from ${url} with method ${method} and timeout ${timeout}`
);
}
fetchData({ url: "https://api.example.com" });
// Fetching data from https://api.example.com with method GET and timeout 5000
fetchData({ url: "https://api.example.com", method: "POST" });
// Fetching data from https://api.example.com with method POST and timeout 5000
fetchData({ url: "https://api.example.com", timeout: 10000 });
// Fetching data from https://api.example.com with method GET and timeout 10000
Why I like this pattern:
- The call site shows named arguments, which is easier to read.
- Each property can have its own default (method = “GET”, timeout = 5000).
- The RequestConfig interface stays simple and optional.
You can also go one level deeper with nested defaults, but I’d keep it readable:
interface RequestOptions {
url: string;
retryCount?: number;
headers?: {
Authorization?: string;
};
}
function request({
url,
retryCount = 3,
headers: { Authorization = "anonymous" } = {},
}: RequestOptions): void {
console.log(url, retryCount, Authorization);
}
request({ url: "https://api.example.com" });
// https://api.example.com 3 anonymous
request({
url: "https://api.example.com",
retryCount: 5,
headers: { Authorization: "Bearer token" },
});
// https://api.example.com 5 Bearer token
This is powerful, but if your parameter list starts to feel like a puzzle, it’s often better to keep the defaults simple and move more complex logic inside the function body.
Real‑world patterns I use default parameters for
Here are some patterns I actually use in projects.
Logging helpers
type LogLevel = "info" | "warn" | "error";
function log(message: string, level: LogLevel = "info"): void {
console.log(`[${level.toUpperCase()}] ${message}`);
}
log("User signed in");
// [INFO] User signed in
log("User not found", "warn");
// [WARN] User not found
This gives you a usable default level while still allowing overrides.
Feature flags
function isFeatureEnabled(
flagName: string,
defaultValue: boolean = false
): boolean {
// In a real app, you’d look this up from config, environment, or remote flags.
console.log(`Checking feature flag: ${flagName}`);
return defaultValue;
}
isFeatureEnabled("new-signup-flow"); // false
isFeatureEnabled("beta-dashboard", true); // true
I use this pattern when integrating with feature flag systems or configuration services.
Simple utility helpers
function padId(id: number, width: number = 5, char: string = "0"): string {
const str = id.toString();
if (str.length >= width) return str;
return char.repeat(width - str.length) + str;
}
padId(42); // "00042"
padId(42, 3); // "042"
padId(42, 4, " "); // " 42"
Callers can usually just call padId(42) and ignore the rest.
Common Issues and Best Practices
Over time, these are the main things I’ve learned to watch for.
1. Don’t overdo parameter defaults
If a function ends up with something like:
function process(
a: number = 1,
b: number = 2,
c: number = 3,
d: number = 4,
e: number = 5
) {
// ...
}
It’s probably doing too much.
When you see more than two or three parameters with defaults, consider switching to an options object:
interface ProcessOptions {
a?: number;
b?: number;
c?: number;
d?: number;
e?: number;
}
function process({
a = 1,
b = 2,
c = 3,
d = 4,
e = 5,
}: ProcessOptions) {
// ...
}
This scales better and is more self‑documenting at the call site.
2. Be careful with null vs undefined
As we saw earlier, defaults apply only on undefined, not null.
If there’s any chance a value comes from JSON or user input that might be null, you still need to guard against that inside the function.
3. Keep default expressions simple
Yes, you can do this:
function getConfig(config: Config = getDefaultConfigFromDisk()) {
// ...
}
But then you’re doing I/O in the parameter list, which can get confusing.
I prefer:
function getConfig(config?: Config) {
const actualConfig = config ?? getDefaultConfigFromDisk();
// ...
}
Short rule: keep defaults lightweight and predictable. Move heavier logic into the body.
4. Consider parameter order
In JavaScript/TypeScript you can have defaults before non‑defaults, but it can lead to awkward calls.
function example(a: number = 1, b: number) {
console.log(a, b);
}
You’d have to explicitly pass undefined to use the default for a and still provide b.