Prerequisites
- Basic TypeScript types knowledge ๐
- JavaScript functions understanding โก
- Arrow functions familiarity ๐ป
What you'll learn
- Type function parameters and returns ๐ฏ
- Master optional and default parameters ๐๏ธ
- Understand function overloading ๐
- Apply advanced function patterns โจ
๐ฏ Introduction
Welcome to the electrifying world of TypeScript functions! โก In this guide, weโll explore how to supercharge your functions with type safety, making them predictable, self-documenting, and bug-resistant.
Think of function types as contracts ๐ - they promise what goes in and what comes out. Just like a vending machine ๐ช that takes coins and gives snacks, TypeScript functions clearly state what they accept and what they return. No surprises, no mysteries!
By the end of this tutorial, youโll be writing functions that are so well-typed, they practically debug themselves! Letโs power up! ๐โโ๏ธ
๐ Understanding Function Types
๐ค What Are Function Types?
Function types in TypeScript are like detailed instruction manuals ๐ for your functions. They specify:
- โจ What parameters the function accepts
- ๐ What type each parameter should be
- ๐ก๏ธ What the function returns
- ๐ Whether parameters are optional
๐ก Why Type Functions?
Typing functions gives you superpowers:
// ๐ฏ Without types - mysterious and error-prone
function calculate(a, b, operation) {
// What are a and b? Numbers? Strings? Objects?
// What operations are valid?
// What does this return?
}
// โจ With types - clear and safe!
function calculate(a: number, b: number, operation: 'add' | 'multiply'): number {
// Crystal clear: numbers in, number out!
return operation === 'add' ? a + b : a * b;
}
๐ง Basic Function Typing
๐ Parameter and Return Types
Letโs start with the fundamentals:
// ๐จ Basic function with types
function greet(name: string): string {
return `Hello, ${name}! ๐`;
}
// ๐ Arrow function with types
const multiply = (x: number, y: number): number => {
return x * y;
};
// ๐ก TypeScript can infer return types
const add = (a: number, b: number) => a + b; // Return type inferred as number
// ๐ฏ Void functions (no return value)
function logMessage(message: string): void {
console.log(`๐ข ${message}`);
// No return statement needed
}
// ๐ Real-world example
interface Product {
id: string;
name: string;
price: number;
}
function calculateDiscount(product: Product, discountPercent: number): number {
const discount = product.price * (discountPercent / 100);
return product.price - discount;
}
const laptop: Product = { id: "1", name: "TypeScript Laptop", price: 999 };
const finalPrice = calculateDiscount(laptop, 20); // $799.20
console.log(`Final price: $${finalPrice} ๐ฐ`);
๐ฎ Optional and Default Parameters
Not all parameters are required:
// ๐ฏ Optional parameters with ?
function buildName(firstName: string, lastName?: string): string {
if (lastName) {
return `${firstName} ${lastName}`;
}
return firstName;
}
console.log(buildName("John")); // "John"
console.log(buildName("John", "Doe")); // "John Doe"
// โจ Default parameters
function createGreeting(
name: string,
greeting: string = "Hello",
emoji: string = "๐"
): string {
return `${greeting}, ${name}! ${emoji}`;
}
console.log(createGreeting("Alice")); // "Hello, Alice! ๐"
console.log(createGreeting("Bob", "Hi")); // "Hi, Bob! ๐"
console.log(createGreeting("Charlie", "Hey", "๐")); // "Hey, Charlie! ๐"
// ๐๏ธ Real-world configuration function
interface ServerConfig {
host: string;
port: number;
ssl: boolean;
timeout: number;
}
function createServer(
host: string,
port: number = 3000,
ssl: boolean = false,
timeout: number = 30000
): ServerConfig {
return { host, port, ssl, timeout };
}
// Various ways to call it
const localServer = createServer("localhost");
const prodServer = createServer("api.example.com", 443, true);
const customServer = createServer("custom.com", 8080, false, 60000);
๐ Rest Parameters
Handle variable numbers of arguments:
// ๐ฏ Rest parameters with spread operator
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(10, 20, 30, 40, 50)); // 150
// ๐จ Combining regular and rest parameters
function formatMessage(template: string, ...values: (string | number)[]): string {
let result = template;
values.forEach((value, index) => {
result = result.replace(`{${index}}`, String(value));
});
return result;
}
console.log(formatMessage("Hello {0}, you have {1} messages! ๐ฌ", "Alice", 5));
// "Hello Alice, you have 5 messages! ๐ฌ"
// ๐ Shopping cart with variable items
interface CartItem {
name: string;
price: number;
emoji: string;
}
function addToCart(userId: string, ...items: CartItem[]): void {
console.log(`๐ Adding ${items.length} items for user ${userId}:`);
items.forEach(item => {
console.log(` ${item.emoji} ${item.name} - $${item.price}`);
});
const total = items.reduce((sum, item) => sum + item.price, 0);
console.log(`๐ฐ Total: $${total.toFixed(2)}`);
}
addToCart("user123",
{ name: "TypeScript Book", price: 39.99, emoji: "๐" },
{ name: "Coffee", price: 4.99, emoji: "โ" },
{ name: "Mechanical Keyboard", price: 149.99, emoji: "โจ๏ธ" }
);
๐ก Function Type Expressions
๐ฏ Defining Function Types
Create reusable function type definitions:
// ๐จ Function type expression
type MathOperation = (x: number, y: number) => number;
const add: MathOperation = (x, y) => x + y;
const subtract: MathOperation = (x, y) => x - y;
const multiply: MathOperation = (x, y) => x * y;
const divide: MathOperation = (x, y) => y !== 0 ? x / y : 0;
// ๐ฎ Event handler types
type ClickHandler = (event: MouseEvent) => void;
type KeyHandler = (event: KeyboardEvent) => void;
const handleClick: ClickHandler = (event) => {
console.log(`Clicked at (${event.clientX}, ${event.clientY}) ๐ฑ๏ธ`);
};
// ๐ Generic function types
type Transformer<T, R> = (input: T) => R;
const numberToString: Transformer<number, string> = (n) => n.toString();
const stringToNumber: Transformer<string, number> = (s) => parseInt(s, 10);
// ๐๏ธ Async function types
type AsyncValidator<T> = (value: T) => Promise<boolean>;
const validateEmail: AsyncValidator<string> = async (email) => {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 100));
return email.includes('@') && email.includes('.');
};
// ๐ฆ Callback types
type Callback<T> = (error: Error | null, result?: T) => void;
function fetchData<T>(url: string, callback: Callback<T>): void {
// Simulated async operation
setTimeout(() => {
if (url.startsWith('http')) {
callback(null, { data: 'Success!' } as T);
} else {
callback(new Error('Invalid URL'));
}
}, 1000);
}
๐๏ธ Function Types in Interfaces
Embed function types in interfaces:
// ๐ฏ Interface with function properties
interface Calculator {
add: (a: number, b: number) => number;
subtract: (a: number, b: number) => number;
multiply: (a: number, b: number) => number;
divide: (a: number, b: number) => number;
history: string[];
lastResult: number;
}
const calculator: Calculator = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => b !== 0 ? a / b : 0,
history: [],
lastResult: 0
};
// ๐ฎ Game controller interface
interface GameController {
// Method signatures
movePlayer(direction: 'up' | 'down' | 'left' | 'right'): void;
jump(): void;
attack(): void;
// Event handlers
onHealthChange: (newHealth: number) => void;
onScoreUpdate: (score: number) => void;
// Properties
playerName: string;
isConnected: boolean;
}
class PlayerController implements GameController {
playerName = "TypeScript Hero";
isConnected = true;
movePlayer(direction: 'up' | 'down' | 'left' | 'right'): void {
console.log(`๐ Moving ${direction}!`);
}
jump(): void {
console.log("๐ฆ Jumping!");
}
attack(): void {
console.log("โ๏ธ Attacking!");
}
onHealthChange = (newHealth: number) => {
console.log(`โค๏ธ Health: ${newHealth}`);
};
onScoreUpdate = (score: number) => {
console.log(`๐ฏ Score: ${score}`);
};
}
๐ Advanced Function Patterns
๐ฏ Function Overloading
Define multiple function signatures:
// ๐จ Function overloads
function formatValue(value: number): string;
function formatValue(value: Date): string;
function formatValue(value: boolean): string;
function formatValue(value: number | Date | boolean): string {
if (typeof value === 'number') {
return `$${value.toFixed(2)}`;
} else if (value instanceof Date) {
return value.toLocaleDateString();
} else {
return value ? "Yes โ
" : "No โ";
}
}
console.log(formatValue(99.99)); // "$99.99"
console.log(formatValue(new Date())); // "12/25/2023"
console.log(formatValue(true)); // "Yes โ
"
// ๐๏ธ Real-world example: Query builder
interface QueryOptions {
limit?: number;
offset?: number;
orderBy?: string;
}
function query(table: string): string;
function query(table: string, id: number): string;
function query(table: string, options: QueryOptions): string;
function query(table: string, idOrOptions?: number | QueryOptions): string {
if (typeof idOrOptions === 'number') {
return `SELECT * FROM ${table} WHERE id = ${idOrOptions}`;
} else if (idOrOptions) {
const { limit, offset, orderBy } = idOrOptions;
let sql = `SELECT * FROM ${table}`;
if (orderBy) sql += ` ORDER BY ${orderBy}`;
if (limit) sql += ` LIMIT ${limit}`;
if (offset) sql += ` OFFSET ${offset}`;
return sql;
}
return `SELECT * FROM ${table}`;
}
console.log(query("users")); // "SELECT * FROM users"
console.log(query("users", 123)); // "SELECT * FROM users WHERE id = 123"
console.log(query("users", { limit: 10, orderBy: "name" }));
// "SELECT * FROM users ORDER BY name LIMIT 10"
๐งโโ๏ธ Generic Functions
Create flexible, reusable functions:
// ๐ฏ Basic generic function
function identity<T>(value: T): T {
return value;
}
const num = identity<number>(42); // Type: number
const str = identity<string>("Hello"); // Type: string
const auto = identity("TypeScript"); // Type inferred as string
// ๐จ Generic array functions
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
function shuffle<T>(array: T[]): T[] {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
// ๐๏ธ Generic with constraints
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(item: T): T {
console.log(`Length: ${item.length}`);
return item;
}
logLength("Hello"); // Works - strings have length
logLength([1, 2, 3]); // Works - arrays have length
// logLength(123); // Error - numbers don't have length
// ๐ Real-world generic: API wrapper
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
async function fetchApi<T>(endpoint: string): Promise<ApiResponse<T>> {
const response = await fetch(`/api/${endpoint}`);
const data = await response.json();
return {
data: data as T,
status: response.status,
message: response.statusText
};
}
// Usage with different types
interface User {
id: string;
name: string;
email: string;
}
interface Product {
id: string;
name: string;
price: number;
}
const userResponse = await fetchApi<User>('users/123');
const productResponse = await fetchApi<Product[]>('products');
๐ฎ Higher-Order Functions
Functions that work with other functions:
// ๐ฏ Function that returns a function
function createMultiplier(factor: number): (value: number) => number {
return (value: number) => value * factor;
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// ๐๏ธ Function decorator pattern
type AsyncFunction<T> = (...args: any[]) => Promise<T>;
function withRetry<T>(
fn: AsyncFunction<T>,
maxRetries: number = 3
): AsyncFunction<T> {
return async (...args: any[]): Promise<T> => {
let lastError: Error | undefined;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn(...args);
} catch (error) {
lastError = error as Error;
console.log(`Attempt ${i + 1} failed, retrying... ๐`);
await new Promise(resolve => setTimeout(resolve, 1000 * i));
}
}
throw lastError || new Error('All retries failed');
};
}
// ๐จ Memoization decorator
function memoize<T extends (...args: any[]) => any>(fn: T): T {
const cache = new Map<string, ReturnType<T>>();
return ((...args: Parameters<T>) => {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('๐ฆ Returning cached result');
return cache.get(key)!;
}
const result = fn(...args);
cache.set(key, result);
return result;
}) as T;
}
// Usage
const expensiveCalculation = memoize((n: number): number => {
console.log('๐งฎ Calculating...');
return n * n * n;
});
console.log(expensiveCalculation(5)); // Calculates
console.log(expensiveCalculation(5)); // Returns cached
// ๐ Composition function
function compose<T>(...functions: Array<(arg: T) => T>): (arg: T) => T {
return (arg: T) => functions.reduceRight((acc, fn) => fn(acc), arg);
}
const addOne = (n: number) => n + 1;
const double = (n: number) => n * 2;
const square = (n: number) => n * n;
const compute = compose(square, double, addOne);
console.log(compute(3)); // ((3 + 1) * 2)ยฒ = 64
๐ Practical Examples
๐ E-Commerce Cart System
Building a type-safe shopping cart:
// ๐๏ธ E-commerce function types
type PriceCalculator = (price: number, quantity: number) => number;
type DiscountStrategy = (subtotal: number) => number;
type TaxCalculator = (subtotal: number, location: string) => number;
interface Product {
id: string;
name: string;
price: number;
category: string;
}
interface CartItem extends Product {
quantity: number;
}
class ShoppingCart {
private items: Map<string, CartItem> = new Map();
private discountStrategy?: DiscountStrategy;
// โ Add item with validation
addItem(
product: Product,
quantity: number = 1,
priceCalculator: PriceCalculator = (price, qty) => price * qty
): void {
if (quantity <= 0) {
throw new Error('Quantity must be positive');
}
const existingItem = this.items.get(product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.set(product.id, { ...product, quantity });
}
const itemTotal = priceCalculator(product.price, quantity);
console.log(`๐ Added ${quantity}x ${product.name} ($${itemTotal.toFixed(2)})`);
}
// ๐ฏ Apply discount strategy
applyDiscount(strategy: DiscountStrategy): void {
this.discountStrategy = strategy;
console.log('๐ธ Discount applied!');
}
// ๐ฐ Calculate total with optional tax
calculateTotal(taxCalculator?: TaxCalculator, location: string = 'US'): {
subtotal: number;
discount: number;
tax: number;
total: number;
} {
// Calculate subtotal
const subtotal = Array.from(this.items.values()).reduce(
(sum, item) => sum + (item.price * item.quantity),
0
);
// Apply discount
const discount = this.discountStrategy ? this.discountStrategy(subtotal) : 0;
const discountedSubtotal = subtotal - discount;
// Calculate tax
const tax = taxCalculator ? taxCalculator(discountedSubtotal, location) : 0;
// Final total
const total = discountedSubtotal + tax;
return { subtotal, discount, tax, total };
}
// ๐ Process checkout with callbacks
checkout(
onSuccess: (orderId: string) => void,
onError: (error: Error) => void,
paymentProcessor: (amount: number) => Promise<string>
): void {
const { total } = this.calculateTotal();
paymentProcessor(total)
.then(orderId => {
console.log('โ
Payment successful!');
this.items.clear();
onSuccess(orderId);
})
.catch(error => {
console.error('โ Payment failed:', error);
onError(error);
});
}
}
// ๐ฏ Usage with different strategies
const cart = new ShoppingCart();
// Products
const laptop: Product = { id: '1', name: 'TypeScript Laptop', price: 999, category: 'electronics' };
const book: Product = { id: '2', name: 'TypeScript Handbook', price: 39.99, category: 'books' };
// Add items
cart.addItem(laptop, 1);
cart.addItem(book, 2);
// Discount strategies
const percentageDiscount: DiscountStrategy = (subtotal) => subtotal * 0.1; // 10% off
const fixedDiscount: DiscountStrategy = (subtotal) => Math.min(50, subtotal); // $50 off
const tieredDiscount: DiscountStrategy = (subtotal) => {
if (subtotal > 1000) return subtotal * 0.15;
if (subtotal > 500) return subtotal * 0.1;
return subtotal * 0.05;
};
// Apply discount
cart.applyDiscount(tieredDiscount);
// Tax calculators
const usTaxCalculator: TaxCalculator = (subtotal, state) => {
const taxRates: Record<string, number> = {
'CA': 0.0725,
'NY': 0.08,
'TX': 0.0625,
'FL': 0.06
};
return subtotal * (taxRates[state] || 0.05);
};
// Calculate total
const totals = cart.calculateTotal(usTaxCalculator, 'CA');
console.log('๐ฐ Order Summary:', totals);
// Mock payment processor
const mockPaymentProcessor = async (amount: number): Promise<string> => {
await new Promise(resolve => setTimeout(resolve, 1000));
if (Math.random() > 0.1) { // 90% success rate
return `ORDER-${Date.now()}`;
}
throw new Error('Payment declined');
};
// Checkout
cart.checkout(
(orderId) => console.log(`๐ Order placed: ${orderId}`),
(error) => console.log(`๐ Checkout failed: ${error.message}`),
mockPaymentProcessor
);
๐ฎ Game Development Functions
Building a game system with typed functions:
// ๐ฎ Game function types
type DamageCalculator = (attacker: Character, defender: Character, skill: Skill) => number;
type StatusEffect = (target: Character, duration: number) => void;
type AIStrategy = (character: Character, enemies: Character[]) => Action;
interface Character {
id: string;
name: string;
health: number;
maxHealth: number;
attack: number;
defense: number;
speed: number;
skills: Skill[];
}
interface Skill {
name: string;
damage: number;
manaCost: number;
cooldown: number;
effect?: StatusEffect;
}
type Action =
| { type: 'attack'; targetId: string; skillName: string }
| { type: 'defend' }
| { type: 'heal' }
| { type: 'flee' };
class BattleSystem {
private characters: Map<string, Character> = new Map();
private turnOrder: string[] = [];
private currentTurn: number = 0;
// โ๏ธ Execute action with custom damage calculation
executeAction(
actorId: string,
action: Action,
damageCalculator: DamageCalculator = this.defaultDamageCalculator
): void {
const actor = this.characters.get(actorId);
if (!actor) return;
switch (action.type) {
case 'attack': {
const target = this.characters.get(action.targetId);
const skill = actor.skills.find(s => s.name === action.skillName);
if (target && skill) {
const damage = damageCalculator(actor, target, skill);
target.health = Math.max(0, target.health - damage);
console.log(`โ๏ธ ${actor.name} uses ${skill.name}! ${damage} damage to ${target.name}`);
if (skill.effect) {
skill.effect(target, 3); // 3 turn duration
}
}
break;
}
case 'defend': {
console.log(`๐ก๏ธ ${actor.name} defends!`);
actor.defense *= 1.5; // Temporary defense boost
break;
}
case 'heal': {
const healAmount = Math.floor(actor.maxHealth * 0.3);
actor.health = Math.min(actor.maxHealth, actor.health + healAmount);
console.log(`๐ ${actor.name} heals for ${healAmount} HP!`);
break;
}
case 'flee': {
console.log(`๐ ${actor.name} flees from battle!`);
this.removeCharacter(actorId);
break;
}
}
}
// ๐ฏ Default damage calculator
private defaultDamageCalculator: DamageCalculator = (attacker, defender, skill) => {
const baseDamage = skill.damage + attacker.attack;
const defense = defender.defense;
const variance = 0.8 + Math.random() * 0.4; // 80-120% damage
return Math.floor(Math.max(1, (baseDamage - defense) * variance));
};
// ๐ง AI turn with strategy
aiTurn(
characterId: string,
strategy: AIStrategy = this.aggressiveStrategy
): void {
const character = this.characters.get(characterId);
if (!character) return;
const enemies = Array.from(this.characters.values())
.filter(c => c.id !== characterId);
const action = strategy(character, enemies);
this.executeAction(characterId, action);
}
// ๐ฎ AI Strategies
private aggressiveStrategy: AIStrategy = (character, enemies) => {
const target = enemies.reduce((weakest, enemy) =>
enemy.health < weakest.health ? enemy : weakest
);
const strongestSkill = character.skills.reduce((best, skill) =>
skill.damage > best.damage ? skill : best
);
return {
type: 'attack',
targetId: target.id,
skillName: strongestSkill.name
};
};
private defensiveStrategy: AIStrategy = (character, enemies) => {
if (character.health < character.maxHealth * 0.3) {
return { type: 'heal' };
}
if (character.health < character.maxHealth * 0.5) {
return { type: 'defend' };
}
// Default to attacking weakest enemy
return this.aggressiveStrategy(character, enemies);
};
// ๐ Turn order based on speed
initializeTurnOrder(
speedModifier: (character: Character) => number = (c) => c.speed
): void {
const characters = Array.from(this.characters.values());
this.turnOrder = characters
.sort((a, b) => speedModifier(b) - speedModifier(a))
.map(c => c.id);
console.log('๐ฏ Turn order:', this.turnOrder.map(id =>
this.characters.get(id)?.name
).join(' โ '));
}
// โ Helper methods
addCharacter(character: Character): void {
this.characters.set(character.id, character);
}
removeCharacter(id: string): void {
this.characters.delete(id);
this.turnOrder = this.turnOrder.filter(tid => tid !== id);
}
}
// ๐ฏ Status effect functions
const burnEffect: StatusEffect = (target, duration) => {
console.log(`๐ฅ ${target.name} is burning for ${duration} turns!`);
// In real implementation, would apply damage over time
};
const freezeEffect: StatusEffect = (target, duration) => {
console.log(`โ๏ธ ${target.name} is frozen for ${duration} turns!`);
target.speed *= 0.5; // Reduce speed
};
// ๐ฎ Usage example
const battle = new BattleSystem();
// Create characters
const hero: Character = {
id: 'hero',
name: 'TypeScript Knight',
health: 100,
maxHealth: 100,
attack: 20,
defense: 15,
speed: 10,
skills: [
{ name: 'Slash', damage: 25, manaCost: 0, cooldown: 0 },
{ name: 'Fire Strike', damage: 35, manaCost: 10, cooldown: 2, effect: burnEffect }
]
};
const enemy: Character = {
id: 'enemy',
name: 'Bug Monster',
health: 80,
maxHealth: 80,
attack: 15,
defense: 10,
speed: 12,
skills: [
{ name: 'Bite', damage: 20, manaCost: 0, cooldown: 0 },
{ name: 'Ice Breath', damage: 30, manaCost: 15, cooldown: 3, effect: freezeEffect }
]
};
// Setup battle
battle.addCharacter(hero);
battle.addCharacter(enemy);
battle.initializeTurnOrder();
// Execute turns
battle.executeAction('hero', {
type: 'attack',
targetId: 'enemy',
skillName: 'Fire Strike'
});
battle.aiTurn('enemy', battle['defensiveStrategy']);
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Optional vs Undefined Parameters
// โ Confusing optional and undefined
function greet(name?: string, greeting: string | undefined) {
// name can be omitted, greeting must be passed (but can be undefined)
}
greet(); // Error! greeting is required
greet("Alice", undefined); // OK
// โ
Clear intent
function betterGreet(name: string, greeting?: string) {
const message = greeting || "Hello";
return `${message}, ${name}!`;
}
betterGreet("Alice"); // "Hello, Alice!"
betterGreet("Bob", "Hi"); // "Hi, Bob!"
๐คฏ Pitfall 2: Type Inference Issues
// โ Poor type inference
const numbers = []; // Type: never[]
numbers.push(1); // Error!
// โ
Explicit typing
const numbers: number[] = [];
numbers.push(1); // Works!
// โ Losing type information
function processValue(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase();
}
return value * 2;
}
const result = processValue("hello"); // Type: string | number (not specific!)
// โ
Function overloads for better types
function betterProcess(value: string): string;
function betterProcess(value: number): number;
function betterProcess(value: string | number): string | number {
if (typeof value === "string") {
return value.toUpperCase();
}
return value * 2;
}
const stringResult = betterProcess("hello"); // Type: string
const numberResult = betterProcess(42); // Type: number
๐ต Pitfall 3: this
Context Issues
// โ Lost context
class Counter {
count = 0;
increment() {
this.count++;
}
}
const counter = new Counter();
const incrementFn = counter.increment;
incrementFn(); // Error! 'this' is undefined
// โ
Arrow functions preserve context
class BetterCounter {
count = 0;
increment = () => {
this.count++;
}
}
// โ
Or use bind
const boundIncrement = counter.increment.bind(counter);
// โ
Or specify this type
interface ClickableElement {
addEventListener(event: string, handler: (this: HTMLElement, e: Event) => void): void;
}
๐ ๏ธ Best Practices
๐ฏ Function Type Best Practices
-
๐ Use Descriptive Parameter Names: Make intent clear
// โ Vague names function calc(x: number, y: number, z: boolean): number // โ Descriptive names function calculatePrice( basePrice: number, taxRate: number, includeShipping: boolean ): number
-
๐๏ธ Prefer Interface for Complex Parameters: Better readability
// โ Too many parameters function createUser( name: string, email: string, age: number, country: string, newsletter: boolean ): User // โ Options object interface CreateUserOptions { name: string; email: string; age: number; country: string; newsletter: boolean; } function createUser(options: CreateUserOptions): User
-
๐จ Use Function Types for Callbacks: Reusable and clear
type SuccessCallback<T> = (data: T) => void; type ErrorCallback = (error: Error) => void; function fetchData<T>( url: string, onSuccess: SuccessCallback<T>, onError: ErrorCallback ): void
-
โจ Return Early for Guard Clauses: Cleaner flow
function processPayment(amount: number, cardNumber?: string): boolean { if (!cardNumber) { console.error("No card number provided"); return false; } if (amount <= 0) { console.error("Invalid amount"); return false; } // Main logic here return true; }
-
๐ Use Generics for Flexibility: Type-safe and reusable
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> { const result = {} as Pick<T, K>; keys.forEach(key => { result[key] = obj[key]; }); return result; }
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Type-Safe Event System
Create a flexible event system with typed functions:
๐ Requirements:
- โ Type-safe event emitter and listeners
- ๐ท๏ธ Different event types with specific payloads
- ๐ฅ Support for multiple listeners per event
- ๐ Event history with timestamps
- ๐จ Filtering and middleware support
๐ Bonus Points:
- Add once listeners
- Implement event priority
- Create async event handlers
๐ก Solution
๐ Click to see solution
// ๐ฏ Type-safe event system
type EventMap = {
login: { userId: string; timestamp: Date };
logout: { userId: string; reason?: string };
purchase: { userId: string; amount: number; items: string[] };
error: { message: string; code: number; stack?: string };
};
type EventKey = keyof EventMap;
type EventHandler<K extends EventKey> = (payload: EventMap[K]) => void | Promise<void>;
type Middleware<K extends EventKey> = (payload: EventMap[K], next: () => void) => void;
interface EventRecord<K extends EventKey = EventKey> {
type: K;
payload: EventMap[K];
timestamp: Date;
}
class TypedEventEmitter {
private handlers: Map<EventKey, Set<EventHandler<any>>> = new Map();
private onceHandlers: Map<EventKey, Set<EventHandler<any>>> = new Map();
private middleware: Map<EventKey, Middleware<any>[]> = new Map();
private history: EventRecord[] = [];
private maxHistorySize: number = 100;
// ๐ Register event handler
on<K extends EventKey>(event: K, handler: EventHandler<K>): () => void {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler);
console.log(`๐ Registered handler for '${event}'`);
// Return unsubscribe function
return () => {
this.handlers.get(event)?.delete(handler);
console.log(`๐ Unregistered handler for '${event}'`);
};
}
// ๐ฏ Register one-time handler
once<K extends EventKey>(event: K, handler: EventHandler<K>): void {
if (!this.onceHandlers.has(event)) {
this.onceHandlers.set(event, new Set());
}
this.onceHandlers.get(event)!.add(handler);
}
// ๐ Add middleware
use<K extends EventKey>(event: K, middleware: Middleware<K>): void {
if (!this.middleware.has(event)) {
this.middleware.set(event, []);
}
this.middleware.get(event)!.push(middleware);
console.log(`๐ Added middleware for '${event}'`);
}
// ๐ Emit event
async emit<K extends EventKey>(event: K, payload: EventMap[K]): Promise<void> {
console.log(`๐ข Emitting '${event}' event`);
// Add to history
this.addToHistory({ type: event, payload, timestamp: new Date() });
// Execute middleware chain
const middlewares = this.middleware.get(event) || [];
let middlewareIndex = 0;
const next = () => {
if (middlewareIndex < middlewares.length) {
const middleware = middlewares[middlewareIndex++];
middleware(payload, next);
} else {
// Execute handlers after middleware
this.executeHandlers(event, payload);
}
};
next();
}
// ๐ฏ Execute handlers
private async executeHandlers<K extends EventKey>(
event: K,
payload: EventMap[K]
): Promise<void> {
// Regular handlers
const handlers = this.handlers.get(event) || new Set();
const promises: Promise<void>[] = [];
for (const handler of handlers) {
const result = handler(payload);
if (result instanceof Promise) {
promises.push(result);
}
}
// Once handlers
const onceHandlers = this.onceHandlers.get(event) || new Set();
for (const handler of onceHandlers) {
const result = handler(payload);
if (result instanceof Promise) {
promises.push(result);
}
}
// Clear once handlers
this.onceHandlers.delete(event);
// Wait for all async handlers
if (promises.length > 0) {
await Promise.all(promises);
}
}
// ๐ Event history management
private addToHistory<K extends EventKey>(record: EventRecord<K>): void {
this.history.push(record);
// Trim history if too large
if (this.history.length > this.maxHistorySize) {
this.history = this.history.slice(-this.maxHistorySize);
}
}
// ๐ Query history
getHistory<K extends EventKey>(
eventType?: K,
filter?: (record: EventRecord<K>) => boolean
): EventRecord<K>[] {
let results = this.history;
if (eventType) {
results = results.filter(record => record.type === eventType);
}
if (filter) {
results = results.filter(record => filter(record as EventRecord<K>));
}
return results as EventRecord<K>[];
}
// ๐ Get event statistics
getStats(): Record<EventKey, { count: number; lastEmitted?: Date }> {
const stats = {} as Record<EventKey, { count: number; lastEmitted?: Date }>;
for (const record of this.history) {
if (!stats[record.type]) {
stats[record.type] = { count: 0 };
}
stats[record.type].count++;
stats[record.type].lastEmitted = record.timestamp;
}
return stats;
}
// ๐งน Clear all handlers
clear(event?: EventKey): void {
if (event) {
this.handlers.delete(event);
this.onceHandlers.delete(event);
this.middleware.delete(event);
} else {
this.handlers.clear();
this.onceHandlers.clear();
this.middleware.clear();
}
}
}
// ๐ฎ Usage example
const eventSystem = new TypedEventEmitter();
// Register handlers
eventSystem.on('login', async ({ userId, timestamp }) => {
console.log(`โ
User ${userId} logged in at ${timestamp.toLocaleTimeString()}`);
// Simulate async operation
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`๐ง Welcome email sent to ${userId}`);
});
eventSystem.on('purchase', ({ userId, amount, items }) => {
console.log(`๐ฐ User ${userId} purchased ${items.length} items for $${amount}`);
console.log(`๐ฆ Items: ${items.join(', ')}`);
});
// One-time handler
eventSystem.once('error', ({ message, code }) => {
console.log(`๐จ First error: ${message} (Code: ${code})`);
});
// Middleware for logging
eventSystem.use('purchase', (payload, next) => {
console.log('๐ Logging purchase...', payload);
next();
});
// Middleware for validation
eventSystem.use('purchase', (payload, next) => {
if (payload.amount < 0) {
console.error('โ Invalid purchase amount!');
return; // Don't call next() to stop the event
}
next();
});
// Emit events
await eventSystem.emit('login', {
userId: 'user123',
timestamp: new Date()
});
await eventSystem.emit('purchase', {
userId: 'user123',
amount: 99.99,
items: ['TypeScript Book', 'Coffee Mug']
});
await eventSystem.emit('error', {
message: 'Connection timeout',
code: 408
});
// This won't trigger the once handler
await eventSystem.emit('error', {
message: 'Another error',
code: 500
});
// Query history
const purchaseHistory = eventSystem.getHistory('purchase');
console.log(`\n๐ Purchase history: ${purchaseHistory.length} events`);
const recentErrors = eventSystem.getHistory('error', record =>
record.timestamp > new Date(Date.now() - 60000) // Last minute
);
// Get statistics
const stats = eventSystem.getStats();
console.log('\n๐ Event statistics:', stats);
// ๐ฏ Advanced: Priority event system
class PriorityEventEmitter extends TypedEventEmitter {
private priorities: Map<EventKey, Map<EventHandler<any>, number>> = new Map();
// Override to add priority
onWithPriority<K extends EventKey>(
event: K,
handler: EventHandler<K>,
priority: number = 0
): () => void {
const unsubscribe = this.on(event, handler);
if (!this.priorities.has(event)) {
this.priorities.set(event, new Map());
}
this.priorities.get(event)!.set(handler, priority);
return () => {
unsubscribe();
this.priorities.get(event)?.delete(handler);
};
}
// Override to sort by priority
protected async executeHandlers<K extends EventKey>(
event: K,
payload: EventMap[K]
): Promise<void> {
const handlers = Array.from(this.handlers.get(event) || []);
const priorities = this.priorities.get(event) || new Map();
// Sort by priority (higher first)
handlers.sort((a, b) => {
const priorityA = priorities.get(a) || 0;
const priorityB = priorities.get(b) || 0;
return priorityB - priorityA;
});
// Execute in priority order
for (const handler of handlers) {
await handler(payload);
}
}
}
๐ Key Takeaways
Youโve mastered function types and parameters in TypeScript! Hereโs what you can now do:
- โ Type function parameters with confidence ๐ช
- โ Define return types for predictable functions ๐ฏ
- โ Use optional and rest parameters effectively ๐๏ธ
- โ Create function overloads for flexibility ๐
- โ Build type-safe APIs with advanced patterns! ๐
Remember: Well-typed functions are self-documenting and catch bugs before they happen. Type everything! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve become a function typing master!
Hereโs what to do next:
- ๐ป Complete the event system exercise
- ๐๏ธ Add types to existing JavaScript functions
- ๐ Learn about generics and conditional types
- ๐ Explore async function patterns!
Remember: Great functions start with great types. Keep building! ๐
Happy coding! ๐๐โจ