Prerequisites
- Basic TypeScript function knowledge ๐
- Understanding of function parameters โก
- TypeScript development environment ๐ป
What you'll learn
- Create functions with optional parameters ๐ฏ
- Set default values for parameters ๐๏ธ
- Combine optional and default parameters ๐
- Apply best practices for flexible APIs โจ
๐ฏ Introduction
Welcome to the wonderful world of optional and default parameters in TypeScript! ๐ In this guide, weโll explore how to create flexible, user-friendly functions that adapt to different use cases.
Imagine building a coffee ordering system โ where customers might want sugar, milk, or specific sizes - but not everyone needs every option. Thatโs where optional and default parameters shine! They let you create functions that work perfectly whether called with one argument or ten.
By the end of this tutorial, youโll be designing elegant APIs that your teammates will love using! Letโs dive in! ๐โโ๏ธ
๐ Understanding Optional and Default Parameters
๐ค What Are Optional Parameters?
Optional parameters are like extras on a pizza ๐. The base pizza is great on its own, but you can add toppings if you want! In TypeScript, optional parameters let callers decide whether to provide certain arguments.
Optional parameters are marked with a ?
after the parameter name:
function greet(name: string, emoji?: string) {
// emoji is optional!
}
๐ก What Are Default Parameters?
Default parameters are like having a favorite coffee order โ. If you donโt specify, you get your usual! They provide fallback values when arguments arenโt supplied:
function makeCoffee(size: string = "medium", sugar: number = 1) {
// If not specified, you get a medium coffee with 1 sugar!
}
๐ฏ Why Use Them?
Hereโs why these features are game-changers:
- Flexible APIs ๐: Functions adapt to different use cases
- Backward Compatibility ๐: Add parameters without breaking existing code
- Cleaner Code โจ: No more checking for undefined values
- Better Developer Experience ๐: Intuitive function interfaces
- Smart Defaults ๐ง : Provide sensible fallbacks
๐ง Basic Syntax and Usage
๐ Optional Parameters
Letโs start with optional parameters:
// ๐จ Basic optional parameter
function greet(name: string, emoji?: string): string {
if (emoji) {
return `Hello ${name}! ${emoji}`;
}
return `Hello ${name}! ๐`;
}
// โจ Using the function
console.log(greet("Alice")); // "Hello Alice! ๐"
console.log(greet("Bob", "๐")); // "Hello Bob! ๐"
// ๐๏ธ Multiple optional parameters
function createProfile(
name: string,
age?: number,
city?: string,
hobby?: string
): string {
let profile = `Name: ${name}`;
if (age) profile += `\nAge: ${age}`;
if (city) profile += `\nCity: ${city}`;
if (hobby) profile += `\nHobby: ${hobby}`;
return profile;
}
// ๐ฏ Call with different combinations
console.log(createProfile("Sarah"));
console.log(createProfile("John", 25));
console.log(createProfile("Emma", 30, "NYC", "TypeScript ๐"));
๐จ Default Parameters
Now letโs explore default parameters:
// โ Function with default values
function orderCoffee(
size: string = "medium",
type: string = "latte",
extras: string[] = []
): string {
const order = `${size} ${type}`;
if (extras.length > 0) {
return `${order} with ${extras.join(", ")} โ`;
}
return `${order} โ`;
}
// ๐ฏ Various ways to call it
console.log(orderCoffee()); // "medium latte โ"
console.log(orderCoffee("large")); // "large latte โ"
console.log(orderCoffee("small", "cappuccino")); // "small cappuccino โ"
console.log(orderCoffee("large", "mocha", ["whip", "caramel"])); // "large mocha with whip, caramel โ"
// ๐ Default parameters with expressions
function createTask(
title: string,
priority: number = 1,
dueDate: Date = new Date(Date.now() + 24 * 60 * 60 * 1000) // Tomorrow!
): object {
return {
title,
priority,
dueDate,
created: new Date(),
emoji: priority > 3 ? "๐ฅ" : "๐"
};
}
๐ Combining Optional and Default
// ๐ฎ Game character creation
function createCharacter(
name: string,
class: string = "warrior",
level?: number,
skills: string[] = ["basic attack"]
): object {
return {
name,
class,
level: level || 1, // If level not provided, default to 1
skills,
emoji: class === "mage" ? "๐ง" : class === "warrior" ? "โ๏ธ" : "๐น"
};
}
// ๐ฏ Flexible usage
const hero1 = createCharacter("Aragorn");
const hero2 = createCharacter("Gandalf", "mage", 50);
const hero3 = createCharacter("Legolas", "archer", undefined, ["arrow shot", "eagle eye"]);
๐ก Practical Examples
๐ Example 1: E-commerce API
Letโs build a flexible product search function:
// ๐๏ธ Product interface
interface Product {
id: string;
name: string;
price: number;
category: string;
inStock: boolean;
emoji: string;
}
// ๐ Flexible search function
function searchProducts(
query: string,
category?: string,
maxPrice?: number,
sortBy: "price" | "name" = "name",
limit: number = 10
): Product[] {
console.log(`๐ Searching for: "${query}"`);
console.log(`๐ Category: ${category || "all"}`);
console.log(`๐ฐ Max price: ${maxPrice ? `$${maxPrice}` : "no limit"}`);
console.log(`๐ Sort by: ${sortBy}`);
console.log(`๐ Limit: ${limit} results\n`);
// Simulated search results
const results: Product[] = [
{ id: "1", name: "TypeScript Handbook", price: 29.99, category: "books", inStock: true, emoji: "๐" },
{ id: "2", name: "Coffee Maker", price: 89.99, category: "appliances", inStock: true, emoji: "โ" },
{ id: "3", name: "Mechanical Keyboard", price: 149.99, category: "electronics", inStock: false, emoji: "โจ๏ธ" }
];
// Filter logic would go here
return results.slice(0, limit);
}
// ๐ฏ Different ways to search
const search1 = searchProducts("typescript");
const search2 = searchProducts("coffee", "appliances");
const search3 = searchProducts("keyboard", undefined, 200, "price", 5);
// ๐ก Using undefined to skip parameters
const search4 = searchProducts("laptop", undefined, undefined, "price");
๐จ Example 2: UI Component Builder
Letโs create a flexible notification system:
// ๐ Notification types
type NotificationType = "success" | "error" | "warning" | "info";
interface NotificationOptions {
title: string;
message: string;
type: NotificationType;
duration: number;
icon: string;
position: string;
sound: boolean;
}
// ๐ฏ Flexible notification function
function showNotification(
title: string,
message: string = "",
type: NotificationType = "info",
duration: number = 3000,
options?: Partial<NotificationOptions>
): void {
// Set up defaults with emojis
const icons: Record<NotificationType, string> = {
success: "โ
",
error: "โ",
warning: "โ ๏ธ",
info: "โน๏ธ"
};
const notification: NotificationOptions = {
title,
message,
type,
duration,
icon: options?.icon || icons[type],
position: options?.position || "top-right",
sound: options?.sound ?? true
};
console.log(`\n${notification.icon} ${notification.type.toUpperCase()}`);
console.log(`Title: ${notification.title}`);
if (notification.message) console.log(`Message: ${notification.message}`);
console.log(`Duration: ${notification.duration}ms`);
console.log(`Position: ${notification.position}`);
console.log(`Sound: ${notification.sound ? "๐" : "๐"}`);
}
// ๐ Usage examples
showNotification("Welcome!");
showNotification("Success!", "Your file has been saved", "success");
showNotification("Error", "Connection failed", "error", 5000);
showNotification(
"Custom Alert",
"This is fancy!",
"warning",
undefined,
{ icon: "๐จ", position: "bottom-left", sound: false }
);
๐ฎ Example 3: Game Configuration
Letโs build a game settings system:
// ๐ฎ Game configuration builder
interface GameConfig {
playerName: string;
difficulty: "easy" | "normal" | "hard" | "nightmare";
startingLives: number;
soundEnabled: boolean;
musicVolume: number;
effectsVolume: number;
controls: "keyboard" | "gamepad" | "touch";
emoji: string;
}
class GameSetup {
// ๐๏ธ Create game with flexible options
static createGame(
playerName: string,
difficulty: GameConfig["difficulty"] = "normal",
customSettings?: Partial<GameConfig>
): GameConfig {
// ๐ฏ Difficulty-based defaults
const difficultyDefaults = {
easy: { lives: 5, emoji: "๐" },
normal: { lives: 3, emoji: "๐ฎ" },
hard: { lives: 2, emoji: "๐ช" },
nightmare: { lives: 1, emoji: "๐" }
};
const defaults = difficultyDefaults[difficulty];
const config: GameConfig = {
playerName,
difficulty,
startingLives: customSettings?.startingLives || defaults.lives,
soundEnabled: customSettings?.soundEnabled ?? true,
musicVolume: customSettings?.musicVolume ?? 70,
effectsVolume: customSettings?.effectsVolume ?? 80,
controls: customSettings?.controls || "keyboard",
emoji: defaults.emoji
};
return config;
}
// ๐จ Pretty print config
static displayConfig(config: GameConfig): void {
console.log(`\n${config.emoji} Game Configuration ${config.emoji}`);
console.log(`๐ค Player: ${config.playerName}`);
console.log(`๐ฏ Difficulty: ${config.difficulty}`);
console.log(`โค๏ธ Lives: ${config.startingLives}`);
console.log(`๐ Sound: ${config.soundEnabled ? "ON" : "OFF"}`);
console.log(`๐ต Music: ${config.musicVolume}%`);
console.log(`๐ฅ Effects: ${config.effectsVolume}%`);
console.log(`๐ฎ Controls: ${config.controls}`);
}
}
// ๐ Different game setups
const casualGame = GameSetup.createGame("Alice", "easy");
const normalGame = GameSetup.createGame("Bob");
const hardcoreGame = GameSetup.createGame(
"Charlie",
"nightmare",
{ soundEnabled: false, controls: "gamepad" }
);
GameSetup.displayConfig(casualGame);
GameSetup.displayConfig(hardcoreGame);
๐ Advanced Concepts
๐งโโ๏ธ Rest Parameters with Defaults
Combine rest parameters with optional/default parameters:
// ๐ฏ Advanced logging function
function log(
level: "info" | "warn" | "error" = "info",
prefix: string = "[LOG]",
...messages: unknown[]
): void {
const emoji = {
info: "โน๏ธ",
warn: "โ ๏ธ",
error: "โ"
};
const timestamp = new Date().toLocaleTimeString();
console.log(`${emoji[level]} ${prefix} ${timestamp}:`, ...messages);
}
// ๐ Usage
log(); // โน๏ธ [LOG] 10:30:45:
log("info", "[APP]", "Server started"); // โน๏ธ [APP] 10:30:45: Server started
log("error", "[DB]", "Connection failed", { code: 500 }); // โ [DB] 10:30:45: Connection failed { code: 500 }
๐๏ธ Builder Pattern with Optional Parameters
Create powerful builders using optional parameters:
// ๐ Advanced configuration builder
class ApiClientBuilder {
private config: {
baseUrl: string;
timeout?: number;
headers?: Record<string, string>;
retries?: number;
cache?: boolean;
logger?: (message: string) => void;
};
constructor(baseUrl: string) {
this.config = { baseUrl };
}
// ๐ง Chainable methods with defaults
withTimeout(timeout: number = 5000): this {
this.config.timeout = timeout;
return this;
}
withHeaders(headers: Record<string, string> = {}): this {
this.config.headers = { ...this.config.headers, ...headers };
return this;
}
withRetries(retries: number = 3): this {
this.config.retries = retries;
return this;
}
withCache(enabled: boolean = true): this {
this.config.cache = enabled;
return this;
}
withLogger(logger?: (message: string) => void): this {
this.config.logger = logger || console.log;
return this;
}
// ๐ Build final client
build(): object {
return {
...this.config,
timeout: this.config.timeout || 5000,
retries: this.config.retries || 3,
cache: this.config.cache ?? true,
emoji: "๐"
};
}
}
// ๐ฏ Usage
const apiClient = new ApiClientBuilder("https://api.example.com")
.withTimeout()
.withRetries(5)
.withCache()
.build();
console.log("API Client:", apiClient);
๐จ Generic Functions with Defaults
// ๐งฌ Generic function with default type parameter
function createCollection<T = string>(
initialItems: T[] = [],
maxSize: number = 100
): {
items: T[];
add: (item: T) => void;
remove: (index: number) => void;
emoji: string;
} {
return {
items: [...initialItems],
add(item: T) {
if (this.items.length < maxSize) {
this.items.push(item);
console.log(`โ Added item (${this.items.length}/${maxSize})`);
} else {
console.log(`โ Collection full!`);
}
},
remove(index: number) {
if (index >= 0 && index < this.items.length) {
this.items.splice(index, 1);
console.log(`โ Removed item at index ${index}`);
}
},
emoji: "๐ฆ"
};
}
// ๐ฎ Usage with different types
const stringCollection = createCollection(); // Default to string
const numberCollection = createCollection<number>([1, 2, 3], 5);
const emojiCollection = createCollection<string>(["๐จ", "๐", "๐ก"]);
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Optional Parameters Order
// โ Wrong - optional parameters must come after required ones!
function badFunction(name?: string, age: number): void {
// TypeScript error: Required parameter cannot follow optional parameter
}
// โ
Correct - required first, then optional
function goodFunction(age: number, name?: string): void {
console.log(`Age: ${age}, Name: ${name || "Anonymous"}`);
}
// ๐ฏ Or use default parameters (can be in any order)
function betterFunction(name: string = "Anonymous", age: number): void {
console.log(`${name} is ${age} years old`);
}
๐คฏ Pitfall 2: undefined vs Default Values
// โ ๏ธ Tricky - passing undefined triggers default, but null doesn't!
function greet(name: string = "Friend"): string {
return `Hello, ${name}!`;
}
console.log(greet()); // "Hello, Friend!" โ
console.log(greet(undefined)); // "Hello, Friend!" โ
console.log(greet(null as any)); // "Hello, null!" ๐ฑ
// โ
Better - handle null explicitly
function safeGreet(name: string | null = "Friend"): string {
return `Hello, ${name || "Friend"}!`;
}
๐ต Pitfall 3: Destructuring with Defaults
// โ Confusing - defaults in wrong place
function processUser({ name, age }: { name?: string; age?: number }) {
// If name/age are undefined, we have no defaults here!
console.log(`${name} is ${age} years old`); // Could be "undefined is undefined years old" ๐ฑ
}
// โ
Correct - defaults in destructuring
function processUser({
name = "Anonymous",
age = 0
}: {
name?: string;
age?: number;
} = {}): void {
console.log(`${name} is ${age} years old`);
}
// ๐ฏ Even better - use interface
interface UserOptions {
name?: string;
age?: number;
emoji?: string;
}
function createUser({
name = "Anonymous",
age = 18,
emoji = "๐ค"
}: UserOptions = {}): void {
console.log(`${emoji} ${name} (${age})`);
}
// Usage
createUser(); // ๐ค Anonymous (18)
createUser({ name: "Alice" }); // ๐ค Alice (18)
createUser({ name: "Bob", age: 25, emoji: "๐งโ๐ป" }); // ๐งโ๐ป Bob (25)
๐ค Pitfall 4: Type Inference with Defaults
// โ ๏ธ Be careful with type inference
function calculate(x = 0, y = 0) {
// x and y are inferred as number
return x + y;
}
// โ This will cause an error
// calculate("hello", "world"); // Error: string not assignable to number
// โ
Be explicit when needed
function flexibleCalculate<T>(x: T = 0 as T, y: T = 0 as T): T {
// More flexible but requires type assertions
return (x as any) + (y as any);
}
๐ ๏ธ Best Practices
1. ๐ฏ Order Parameters by Importance
// โ
Good - most important/required first
function sendEmail(
to: string,
subject: string,
body: string,
cc?: string[],
attachments?: File[],
priority: "low" | "normal" | "high" = "normal"
) { /* ... */ }
2. ๐ Use Descriptive Defaults
// โ
Good - defaults are self-documenting
function createButton(
text: string,
style: "primary" | "secondary" | "danger" = "primary",
size: "small" | "medium" | "large" = "medium",
disabled: boolean = false
) { /* ... */ }
3. ๐ก๏ธ Validate Optional Parameters
// โ
Good - validate optional inputs
function processPayment(
amount: number,
currency: string = "USD",
description?: string
): void {
if (amount <= 0) {
throw new Error("โ Amount must be positive");
}
if (description && description.length > 100) {
console.warn("โ ๏ธ Description truncated to 100 characters");
description = description.substring(0, 100);
}
console.log(`๐ณ Processing ${currency}${amount}`);
}
4. ๐จ Use Object Parameters for Many Options
// โ Too many parameters
function createBadUser(
name: string,
email?: string,
age?: number,
country?: string,
newsletter?: boolean,
theme?: string
) { /* ... */ }
// โ
Better - use options object
interface UserOptions {
email?: string;
age?: number;
country?: string;
newsletter?: boolean;
theme?: string;
}
function createGoodUser(name: string, options: UserOptions = {}): void {
const {
email = "[email protected]",
age = 0,
country = "Unknown",
newsletter = false,
theme = "light"
} = options;
console.log(`๐ค Creating user: ${name}`);
}
5. โจ Document Default Behavior
/**
* Formats a date string
* @param date - The date to format
* @param format - Format string (default: "YYYY-MM-DD")
* @param locale - Locale for formatting (default: "en-US")
* @returns Formatted date string
*/
function formatDate(
date: Date,
format: string = "YYYY-MM-DD",
locale: string = "en-US"
): string {
// Implementation
return date.toLocaleDateString(locale);
}
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Flexible API Client
Create a type-safe HTTP client with flexible request options:
๐ Requirements:
- โ Support GET, POST, PUT, DELETE methods
- ๐ง Optional timeout and retry configuration
- ๐จ Custom headers with defaults
- ๐ Request/response logging options
- ๐ก๏ธ Error handling with retries
๐ Bonus Points:
- Add request caching
- Implement request queuing
- Create response transformers
๐ก Solution
๐ Click to see solution
// ๐ฏ Flexible HTTP client with optional/default parameters
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
interface RequestOptions {
headers?: Record<string, string>;
timeout?: number;
retries?: number;
cache?: boolean;
logLevel?: "none" | "basic" | "verbose";
}
interface ApiResponse<T> {
data: T;
status: number;
cached: boolean;
duration: number;
}
class ApiClient {
constructor(
private baseUrl: string,
private defaultHeaders: Record<string, string> = {
"Content-Type": "application/json"
}
) {}
// ๐ Main request method with flexible options
async request<T>(
endpoint: string,
method: HttpMethod = "GET",
body?: any,
options: RequestOptions = {}
): Promise<ApiResponse<T>> {
// ๐จ Merge options with defaults
const {
headers = {},
timeout = 5000,
retries = 3,
cache = method === "GET",
logLevel = "basic"
} = options;
const url = `${this.baseUrl}${endpoint}`;
const startTime = Date.now();
// ๐ Logging
if (logLevel !== "none") {
console.log(`\n๐ ${method} ${url}`);
if (logLevel === "verbose") {
console.log("๐ Headers:", { ...this.defaultHeaders, ...headers });
if (body) console.log("๐ฆ Body:", body);
}
}
// ๐ Retry logic
let lastError: Error | null = null;
for (let attempt = 0; attempt <= retries; attempt++) {
try {
// Simulated fetch (replace with real implementation)
const response = await this.simulatedFetch<T>(url, {
method,
headers: { ...this.defaultHeaders, ...headers },
body: body ? JSON.stringify(body) : undefined,
timeout
});
const duration = Date.now() - startTime;
if (logLevel !== "none") {
console.log(`โ
Success (${duration}ms)`);
}
return {
data: response,
status: 200,
cached: false,
duration
};
} catch (error) {
lastError = error as Error;
if (attempt < retries) {
console.log(`โ ๏ธ Attempt ${attempt + 1} failed, retrying...`);
await this.delay(1000 * (attempt + 1)); // Exponential backoff
}
}
}
throw new Error(`โ Request failed after ${retries + 1} attempts: ${lastError?.message}`);
}
// ๐ฏ Convenience methods with smart defaults
get<T>(endpoint: string, options?: RequestOptions): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, "GET", undefined, options);
}
post<T>(
endpoint: string,
data?: any,
options?: RequestOptions
): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, "POST", data, {
cache: false,
...options
});
}
put<T>(
endpoint: string,
data?: any,
options?: RequestOptions
): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, "PUT", data, {
cache: false,
...options
});
}
delete<T>(
endpoint: string,
options?: RequestOptions
): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, "DELETE", undefined, {
cache: false,
...options
});
}
// ๐ ๏ธ Helper methods
private async simulatedFetch<T>(
url: string,
options: any
): Promise<T> {
// Simulate network delay
await this.delay(Math.random() * 1000);
// Simulate successful response
return { message: "Success!", timestamp: new Date() } as any;
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// ๐ฎ Usage examples
const api = new ApiClient("https://api.example.com");
// Simple GET request with defaults
api.get("/users");
// POST with custom options
api.post(
"/users",
{ name: "Alice", email: "[email protected]" },
{ timeout: 10000, logLevel: "verbose" }
);
// GET with minimal logging and no retries
api.get("/health", { retries: 0, logLevel: "none" });
// PUT with custom headers
api.put(
"/users/123",
{ name: "Alice Updated" },
{ headers: { "X-API-Key": "secret" } }
);
๐ Key Takeaways
Youโve mastered optional and default parameters! Hereโs what you can now do:
- โ Create flexible functions that adapt to different use cases ๐ช
- โ Set smart defaults that make APIs intuitive ๐ก๏ธ
- โ Combine optional and default parameters effectively ๐ฏ
- โ Avoid common pitfalls with parameter ordering ๐
- โ Build user-friendly APIs that developers love! ๐
Remember: Good defaults make happy developers! Your functions should be easy to use for simple cases but flexible enough for complex ones. ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered optional and default parameters!
Hereโs what to do next:
- ๐ป Complete the API client exercise above
- ๐๏ธ Refactor existing functions to use better defaults
- ๐ Move on to our next tutorial: Rest Parameters and Spread Operator
- ๐ Create a library with a beautifully flexible API!
Remember: The best APIs feel natural to use. With optional and default parameters, youโre building functions that are a joy to work with! ๐
Happy coding! ๐๐โจ