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 10000Why 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 tokenThis 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.
In practice, I keep non‑default parameters first and put defaulted ones after them. If you need more flexibility, again, an options object is usually clearer.
When to use default parameters
I reach for default parameters when:
- I want the function to be easy to call with minimal arguments.
- There’s a clear “typical” value for a parameter.
- I want to avoid
undefinedchecks everywhere in the body. - I’m designing helpers, logging utilities, or small config‑style functions.
I usually avoid them when:
- The number of parameters is large and growing.
- The default value is expensive to compute.
- I need to handle complex logic around
nullvsundefinedvs “not passed”.
Try it yourself (small exercise)
If you want a quick exercise, try this:
- Write a function formatPrice(amount: number, currency: string = “USD”, locale: string = “en-US”) that returns a formatted currency string using Intl.NumberFormat.
- Call it with just the amount, then with all arguments, and see how the defaults behave.
Here’s one possible solution:
function formatPrice(
amount: number,
currency: string = "USD",
locale: string = "en-US"
): string {
return new Intl.NumberFormat(locale, {
style: "currency",
currency,
}).format(amount);
}
console.log(formatPrice(19.99)); // $19.99 (in US locale)
console.log(formatPrice(19.99, "EUR", "de-DE")); // 19,99 €
This is a nice example that feels realistic if you’re working on a web app for users in the United States.
FAQ: common questions about TypeScript default parameters
Can I use default parameters in arrow functions?
Yes. Arrow functions support default parameters just like regular functions.
tsconst multiply = (a: number, b: number = 2): number => a * b;
multiply(5); // 10
multiply(5, 3); // 15
Can I use default parameters in class methods and constructors?
Yes. You can use them in both methods and constructors.
tsclass User {
constructor(
public name: string,
public role: string = “user”
) {}
greet(greeting: string = “Hello”): string {
return `${greeting}, ${this.name}!`;
}
}
const user = new User(“David”);
console.log(user.role); // “user”
console.log(user.greet()); // “Hello, David!”
Do default parameters affect TypeScript types?
TypeScript infers the parameter type from the annotation and the default together. If you write:
tsfunction demo(count = 10) {
// …
}
How are default parameters different from function overloads?
Function overloads are a type‑level feature in TypeScript to describe different call signatures. Default parameters are a runtime feature from JavaScript to provide actual values.
You can use them together, but they solve different problems. Defaults give you fallback values at runtime; overloads give you better type checking for multiple ways to call the function.
Summary
Default parameters are one of those simple features that quietly make TypeScript code much easier to work with. They:
- Give your functions sensible, documented defaults.
- Reduce if (!value) checks and undefined handling.
- Make your APIs easier to call and harder to misuse.
If you get into the habit of using them thoughtfully, especially in small helpers, option objects, and class methods, you’ll notice your code getting cleaner and more predictable.
You may read:
- Use ForwardRef in TypeScript
- TypeScript Event Types
- React’s useContext Hook with TypeScript
- TypeScript Best Practices

Bijay Kumar is an experienced Python and AI professional who enjoys helping developers learn modern technologies through practical tutorials and examples. His expertise includes Python development, Machine Learning, Artificial Intelligence, automation, and data analysis using libraries like Pandas, NumPy, TensorFlow, Matplotlib, SciPy, and Scikit-Learn. At PythonGuides.com, he shares in-depth guides designed for both beginners and experienced developers. More about us.