Functions don't always need every parameter. TypeScript provides two powerful ways to make parameters flexible: optional parameters (using ?) and default parameters (with default values). These features make your functions more versatile and easier to use. Let's master both approaches and learn when to use each!
Optional Parameters Deep Dive
Optional parameters use ? after the parameter name. They can be omitted when calling the function, and their value will be undefined if not provided.
Basic Optional Parameters
// Function with optional parameters
function greet(name: string, greeting?: string): string {
if (greeting) {
return `${greeting}, ${name}!`;
}
return `Hello, ${name}!`;
}
// Call with or without optional parameter
console.log(greet("Yusuf")); // "Hello, Yusuf!"
console.log(greet("Yusuf", "Hi")); // "Hi, Yusuf!"
console.log(greet("Amina", "Good day")); // "Good day, Amina!"
// Multiple optional parameters
function createUser(
name: string,
age?: number,
email?: string,
phone?: string
): { name: string; age?: number; email?: string; phone?: string } {
return { name, age, email, phone };
}
console.log(createUser("Yusuf"));
// { name: "Yusuf" }
console.log(createUser("Amina", 30));
// { name: "Amina", age: 30 }
console.log(createUser("Chidi", 28, "chidi@example.com"));
// { name: "Chidi", age: 28, email: "chidi@example.com" }
console.log(createUser("Fatima", 25, "fatima@example.com", "+234-803-123-4567"));
// { name: "Fatima", age: 25, email: "fatima@example.com", phone: "+234-803-123-4567" }Handling Optional Parameters
// Check if optional parameter exists
function displayPrice(price: number, currency?: string): string {
// Method 1: if statement
if (currency !== undefined) {
return `${currency}${price.toFixed(2)}`;
}
return `₦${price.toFixed(2)}`;
}
console.log(displayPrice(5000)); // "₦5000.00"
console.log(displayPrice(5000, "$")); // "$5000.00"
// Method 2: Nullish coalescing operator
function formatPrice(price: number, currency?: string): string {
const currencySymbol = currency ?? "₦";
return `${currencySymbol}${price.toFixed(2)}`;
}
console.log(formatPrice(10000)); // "₦10000.00"
console.log(formatPrice(10000, "€")); // "€10000.00"
// Method 3: Logical OR (be careful with falsy values!)
function logMessage(message: string, prefix?: string): void {
const finalPrefix = prefix || "[LOG]";
console.log(`${finalPrefix} ${message}`);
}
logMessage("User logged in");
// "[LOG] User logged in"
logMessage("User logged in", "[INFO]");
// "[INFO] User logged in"
// Method 4: Conditional (ternary) operator
function createGreeting(name: string, time?: string): string {
return time
? `Good ${time}, ${name}!`
: `Hello, ${name}!`;
}
console.log(createGreeting("Yusuf"));
// "Hello, Yusuf!"
console.log(createGreeting("Yusuf", "morning"));
// "Good morning, Yusuf!"⚠️ Optional Parameter Gotchas
- Optional parameters are
undefinedwhen not provided, notnull - Optional parameters must come after required parameters
- Use
!== undefinedor??for checking, not justif (param) - Empty string
""and0are valid values, not the same as missing
Default Parameters Deep Dive
Default parameters provide fallback values when arguments are not supplied. They're more explicit than optional parameters and eliminate the need for undefined checks.
Basic Default Parameters
// Function with default parameters
function greet(name: string, greeting: string = "Hello"): string {
return `${greeting}, ${name}!`;
}
console.log(greet("Yusuf")); // "Hello, Yusuf!"
console.log(greet("Yusuf", "Hi")); // "Hi, Yusuf!"
console.log(greet("Amina", "Good day")); // "Good day, Amina!"
// Multiple default parameters
function createConnection(
host: string,
port: number = 8080,
secure: boolean = false,
timeout: number = 30000
): string {
const protocol = secure ? "https" : "http";
return `${protocol}://${host}:${port} (timeout: ${timeout}ms)`;
}
console.log(createConnection("localhost"));
// "http://localhost:8080 (timeout: 30000ms)"
console.log(createConnection("api.example.com", 443, true));
// "https://api.example.com:443 (timeout: 30000ms)"
console.log(createConnection("localhost", 3000, false, 60000));
// "http://localhost:3000 (timeout: 60000ms)"
// Default can be any expression
function logWithTimestamp(
message: string,
timestamp: string = new Date().toISOString()
): void {
console.log(`[${timestamp}] ${message}`);
}
logWithTimestamp("Application started");
// "[2024-12-25T10:30:00.000Z] Application started"
logWithTimestamp("Custom timestamp", "2024-01-01T00:00:00.000Z");
// "[2024-01-01T00:00:00.000Z] Custom timestamp"Complex Default Values
// Default with object
function createUser(
name: string,
options: { age?: number; email?: string } = {}
): { name: string; age?: number; email?: string } {
return {
name,
age: options.age,
email: options.email
};
}
console.log(createUser("Yusuf"));
// { name: "Yusuf" }
console.log(createUser("Amina", { age: 30 }));
// { name: "Amina", age: 30 }
console.log(createUser("Chidi", { age: 28, email: "chidi@example.com" }));
// { name: "Chidi", age: 28, email: "chidi@example.com" }
// Default with array
function processItems(
items: string[],
options: string[] = ["option1", "option2"]
): void {
console.log("Items:", items);
console.log("Options:", options);
}
processItems(["A", "B"]);
// Items: ["A", "B"]
// Options: ["option1", "option2"]
processItems(["X", "Y"], ["custom1", "custom2"]);
// Items: ["X", "Y"]
// Options: ["custom1", "custom2"]
// Default from function call
function getDefaultPort(): number {
return 8080;
}
function startServer(
host: string,
port: number = getDefaultPort()
): void {
console.log(`Server starting on ${host}:${port}`);
}
startServer("localhost");
// "Server starting on localhost:8080"
// Default with calculation
function calculateDiscount(
price: number,
discountPercent: number = price > 100000 ? 15 : 10
): number {
return price - (price * discountPercent / 100);
}
console.log(calculateDiscount(50000)); // 45000 (10% off)
console.log(calculateDiscount(150000)); // 127500 (15% off)Optional vs Default: When to Use Each
| Scenario | Use Optional (?) | Use Default (=) |
|---|---|---|
| Have sensible default | ❌ | ✅ Yes |
| No clear default | ✅ Yes | ❌ |
| Need to check if provided | ✅ Yes | ❌ |
| Always want a value | ❌ | ✅ Yes |
| Parameter might be undefined | ✅ Yes | ⚠️ Careful |
// Use DEFAULT when you have a sensible fallback
function createLogger(
name: string,
level: string = "INFO", // Clear default: INFO level
enabled: boolean = true // Clear default: enabled
): void {
if (enabled) {
console.log(`[${level}] ${name}`);
}
}
createLogger("App"); // Uses defaults
createLogger("App", "ERROR"); // Override level
createLogger("App", "DEBUG", false); // Override both
// Use OPTIONAL when there's no clear default
function sendEmail(
to: string,
subject: string,
body: string,
cc?: string, // No sensible default for CC
bcc?: string // No sensible default for BCC
): void {
console.log(`To: ${to}`);
console.log(`Subject: ${subject}`);
if (cc) {
console.log(`CC: ${cc}`);
}
if (bcc) {
console.log(`BCC: ${bcc}`);
}
}
sendEmail("user@example.com", "Hello", "Message body");
sendEmail("user@example.com", "Hello", "Message body", "cc@example.com");
// Use OPTIONAL when you need to distinguish "not provided" from "default"
function updateUser(
id: number,
name?: string,
email?: string
): void {
console.log(`Updating user ${id}`);
// Only update provided fields
if (name !== undefined) {
console.log(`Setting name to: ${name}`);
}
if (email !== undefined) {
console.log(`Setting email to: ${email}`);
}
}
updateUser(1); // Update nothing
updateUser(1, "New Name"); // Update only name
updateUser(1, undefined, "new@example.com"); // Update only emailGeneral Rule: Use default parameters when you have a sensible fallback value. Use optional parameters when there's no obvious default or when you need to distinguish between "not provided" and "provided with default value."
Parameter Destructuring
Destructuring lets you extract values from objects or arrays directly in function parameters, making your code more concise.
Object Destructuring in Parameters
// Without destructuring
function createUserOld(user: { name: string; age: number; email: string }): void {
console.log(user.name);
console.log(user.age);
console.log(user.email);
}
// With destructuring (cleaner!)
function createUser({ name, age, email }: { name: string; age: number; email: string }): void {
console.log(name);
console.log(age);
console.log(email);
}
createUser({ name: "Yusuf", age: 25, email: "yusuf@example.com" });
// Type alias for readability
type UserParams = {
name: string;
age: number;
email: string;
};
function registerUser({ name, age, email }: UserParams): void {
console.log(`Registering ${name}, age ${age}`);
}
// Destructuring with optional properties
type ConfigParams = {
host: string;
port?: number;
secure?: boolean;
};
function connect({ host, port, secure }: ConfigParams): void {
console.log(`Host: ${host}`);
console.log(`Port: ${port ?? 8080}`);
console.log(`Secure: ${secure ?? false}`);
}
connect({ host: "localhost" });
connect({ host: "api.example.com", port: 443, secure: true });Destructuring with Default Values
// Destructuring with defaults
function createConnection({
host,
port = 8080,
secure = false,
timeout = 30000
}: {
host: string;
port?: number;
secure?: boolean;
timeout?: number;
}): void {
const protocol = secure ? "https" : "http";
console.log(`${protocol}://${host}:${port} (timeout: ${timeout}ms)`);
}
createConnection({ host: "localhost" });
// "http://localhost:8080 (timeout: 30000ms)"
createConnection({ host: "api.example.com", port: 443, secure: true });
// "https://api.example.com:443 (timeout: 30000ms)"
// With type alias (recommended)
type ConnectionOptions = {
host: string;
port?: number;
secure?: boolean;
timeout?: number;
};
function startServer({
host,
port = 8080,
secure = false,
timeout = 30000
}: ConnectionOptions): void {
console.log(`Server starting on ${host}:${port}`);
}
// Default entire parameter object
type ServerConfig = {
host: string;
port: number;
secure: boolean;
};
function launchServer(
config: ServerConfig = {
host: "localhost",
port: 8080,
secure: false
}
): void {
console.log(`Launching on ${config.host}:${config.port}`);
}
launchServer(); // Uses all defaults
launchServer({ host: "0.0.0.0", port: 3000, secure: true });Nested Destructuring
// Nested object destructuring
type User = {
name: string;
contact: {
email: string;
phone?: string;
};
address: {
city: string;
state: string;
};
};
function displayUserInfo({
name,
contact: { email, phone },
address: { city, state }
}: User): void {
console.log(`Name: ${name}`);
console.log(`Email: ${email}`);
console.log(`Phone: ${phone ?? "Not provided"}`);
console.log(`Location: ${city}, ${state}`);
}
displayUserInfo({
name: "Yusuf Ibrahim",
contact: {
email: "yusuf@example.com",
phone: "+234-803-123-4567"
},
address: {
city: "Lagos",
state: "Lagos State"
}
});
// Nested with defaults
type ProductOptions = {
name: string;
pricing: {
amount: number;
currency?: string;
};
metadata?: {
tags?: string[];
};
};
function createProduct({
name,
pricing: { amount, currency = "₦" },
metadata = { tags: [] }
}: ProductOptions): void {
console.log(`Product: ${name}`);
console.log(`Price: ${currency}${amount}`);
console.log(`Tags: ${metadata.tags?.join(", ") || "None"}`);
}
createProduct({
name: "Laptop",
pricing: { amount: 450000 }
});
createProduct({
name: "Mouse",
pricing: { amount: 5000, currency: "$" },
metadata: { tags: ["electronics", "accessories"] }
});Array Destructuring in Parameters
// Array destructuring
function displayCoordinates([x, y]: [number, number]): void {
console.log(`X: ${x}, Y: ${y}`);
}
displayCoordinates([10, 20]); // "X: 10, Y: 20"
// With default values
function plotPoint([x = 0, y = 0]: [number?, number?] = [0, 0]): void {
console.log(`Plotting at (${x}, ${y})`);
}
plotPoint(); // "Plotting at (0, 0)"
plotPoint([5]); // "Plotting at (5, 0)"
plotPoint([5, 10]); // "Plotting at (5, 10)"
// Rest in array destructuring
function sum([first, ...rest]: number[]): number {
return first + rest.reduce((a, b) => a + b, 0);
}
console.log(sum([1, 2, 3, 4, 5])); // 15
// Mixed array and object destructuring
function processData([id, { name, email }]: [number, { name: string; email: string }]): void {
console.log(`ID: ${id}`);
console.log(`Name: ${name}`);
console.log(`Email: ${email}`);
}
processData([1, { name: "Yusuf", email: "yusuf@example.com" }]);Practical Examples
Example 1: Configuration Builder
// Configuration options
type DatabaseConfig = {
host: string;
port?: number;
username?: string;
password?: string;
database: string;
ssl?: boolean;
poolSize?: number;
timeout?: number;
};
function createDatabaseConfig({
host,
port = 5432,
username = "admin",
password,
database,
ssl = false,
poolSize = 10,
timeout = 30000
}: DatabaseConfig): string {
const config = {
host,
port,
username,
password: password ? "****" : undefined,
database,
ssl,
poolSize,
timeout
};
return JSON.stringify(config, null, 2);
}
// Minimal configuration
console.log(createDatabaseConfig({
host: "localhost",
database: "myapp"
}));
// Full configuration
console.log(createDatabaseConfig({
host: "db.example.com",
port: 3306,
username: "dbuser",
password: "secret123",
database: "production",
ssl: true,
poolSize: 20,
timeout: 60000
}));Example 2: API Request Builder
// API request options
type RequestOptions = {
url: string;
method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
headers?: Record<string, string>;
body?: any;
timeout?: number;
retry?: {
maxAttempts?: number;
delay?: number;
};
};
async function makeRequest({
url,
method = "GET",
headers = { "Content-Type": "application/json" },
body,
timeout = 30000,
retry = { maxAttempts: 3, delay: 1000 }
}: RequestOptions): Promise<any> {
console.log(`Making ${method} request to ${url}`);
console.log(`Headers:`, headers);
console.log(`Timeout: ${timeout}ms`);
console.log(`Retry: ${retry.maxAttempts} attempts with ${retry.delay}ms delay`);
if (body) {
console.log(`Body:`, JSON.stringify(body));
}
// Actual fetch implementation would go here
return { success: true };
}
// Simple GET request
await makeRequest({
url: "https://api.example.com/users"
});
// POST request with body
await makeRequest({
url: "https://api.example.com/users",
method: "POST",
body: {
name: "Yusuf Ibrahim",
email: "yusuf@example.com"
}
});
// Full customization
await makeRequest({
url: "https://api.example.com/orders",
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer token123"
},
body: { orderId: 1001 },
timeout: 60000,
retry: {
maxAttempts: 5,
delay: 2000
}
});Example 3: Email Sender
// Email options
type EmailOptions = {
to: string | string[];
subject: string;
body: string;
from?: string;
cc?: string | string[];
bcc?: string | string[];
replyTo?: string;
attachments?: Array<{
filename: string;
path: string;
}>;
priority?: "high" | "normal" | "low";
};
function sendEmail({
to,
subject,
body,
from = "noreply@example.com",
cc,
bcc,
replyTo,
attachments = [],
priority = "normal"
}: EmailOptions): void {
console.log("=== Sending Email ===");
console.log(`From: ${from}`);
console.log(`To: ${Array.isArray(to) ? to.join(", ") : to}`);
if (cc) {
console.log(`CC: ${Array.isArray(cc) ? cc.join(", ") : cc}`);
}
if (bcc) {
console.log(`BCC: ${Array.isArray(bcc) ? bcc.join(", ") : bcc}`);
}
console.log(`Subject: ${subject}`);
console.log(`Body: ${body}`);
console.log(`Priority: ${priority}`);
if (replyTo) {
console.log(`Reply-To: ${replyTo}`);
}
if (attachments.length > 0) {
console.log(`Attachments: ${attachments.map(a => a.filename).join(", ")}`);
}
}
// Simple email
sendEmail({
to: "user@example.com",
subject: "Welcome!",
body: "Welcome to our platform!"
});
// Email with CC
sendEmail({
to: "user@example.com",
subject: "Order Confirmation",
body: "Your order #1001 has been confirmed.",
cc: "manager@example.com"
});
// Full email with all options
sendEmail({
to: ["user1@example.com", "user2@example.com"],
subject: "Important Update",
body: "Please review the attached document.",
from: "admin@example.com",
cc: "team@example.com",
bcc: ["archive@example.com"],
replyTo: "support@example.com",
attachments: [
{ filename: "document.pdf", path: "/files/document.pdf" }
],
priority: "high"
});Test Your Knowledge
Type Safety Check
Which function signature is correct?
// Option A
function greet(name: string, greeting?: string, title: string): string {
return `${greeting} ${title} ${name}`;
}
// Option B
function greet(name: string, greeting: string = "Hello", title?: string): string {
return title ? `${greeting} ${title} ${name}` : `${greeting} ${name}`;
}
// Option C
function greet(name?: string, greeting: string): string {
return `${greeting} ${name}`;
}
// Option D
function greet(name: string, greeting: string = "Hello", title: string = "Mr."): string {
return `${greeting} ${title} ${name}`;
}Common TypeScript Error
// Function with parameter order error
function createUser(
name: string,
age?: number,
email: string // Error: required after optional!
): void {
console.log(name, age, email);
}
// Calling function incorrectly
function greet(name: string, greeting: string = "Hello"): string {
return `${greeting}, ${name}!`;
}
// This doesn't work as expected
greet("Yusuf", undefined, "Hi"); // Error: too many arguments❌ A required parameter cannot follow an optional parameter. Expected 1-2 arguments, but got 3.
What's Wrong?
Required parameters must come before optional/default parameters. You also cannot pass extra arguments beyond what the function accepts.
// Fixed: Proper parameter order
function createUser(
name: string,
email: string,
age?: number // Optional at the end
): void {
console.log(name, email, age);
}
// Fixed: Call with correct number of arguments
function greet(name: string, greeting: string = "Hello"): string {
return `${greeting}, ${name}!`;
}
greet("Yusuf"); // ✅ Uses default: "Hello, Yusuf!"
greet("Yusuf", "Hi"); // ✅ "Hi, Yusuf!"Solution: Reorder parameters: required first, then optional/default
Best Practices
- Use default parameters when you have sensible defaults
- Use optional parameters when there's no clear default
- Required first, then defaults, then optional - proper order
- Use type aliases for complex parameter objects
- Destructure parameters for cleaner code
- Check with !== undefined not just truthiness
- Use nullish coalescing (??) for default values
- Document complex parameters with JSDoc comments
- Don't overuse optional parameters - too many makes functions hard to use
- Consider options object for 3+ optional parameters
// ✅ Good: Type alias with destructuring and defaults
type ServerOptions = {
host: string;
port?: number;
secure?: boolean;
timeout?: number;
};
function startServer({
host,
port = 8080,
secure = false,
timeout = 30000
}: ServerOptions): void {
console.log(`Starting ${secure ? "secure" : "normal"} server on ${host}:${port}`);
}
// ✅ Good: Proper parameter order
function createUser(
name: string, // Required
email: string, // Required
age: number = 18, // Default
phone?: string // Optional
): void {
console.log(name, email, age, phone);
}
// ✅ Good: Options object for many parameters
type EmailConfig = {
to: string;
subject: string;
body: string;
from?: string;
cc?: string;
priority?: string;
};
function sendEmail(config: EmailConfig): void {
// Clean function signature
}
// ❌ Avoid: Too many individual parameters
function badEmail(
to: string,
subject: string,
body: string,
from?: string,
cc?: string,
bcc?: string,
replyTo?: string,
priority?: string
): void {
// Hard to use and maintain
}
// ❌ Avoid: Wrong parameter order
function wrongOrder(
name?: string, // Optional before required - ERROR!
age: number
): void {}
// ✅ Good: Check for undefined explicitly
function process(value?: number): void {
if (value !== undefined) {
console.log(value * 2);
}
}Key Takeaways
- Optional parameters use
?and areundefinedwhen omitted - Default parameters use
=and always have a value - Required parameters must come before optional/default ones
- Use
!== undefinedor??to check optional parameters - Prefer defaults when you have sensible fallback values
- Use optional when there's no clear default or you need to distinguish "not provided"
- Destructuring makes parameter handling cleaner and more readable
- Combine destructuring with defaults for maximum flexibility
- Use options objects for functions with many parameters
- Type aliases improve readability of complex parameter types
What's Next?
Excellent! You've mastered optional and default parameters, including advanced destructuring patterns. Next, we'll explore Function Overloading—TypeScript's way to define multiple function signatures for the same function. You'll learn how to create functions that behave differently based on the arguments passed, making your APIs more flexible and type-safe!
Get ready to create sophisticated, overloaded functions!