Functions
Introduction
Functions are the building blocks of any program. They are reusable pieces of code that perform specific tasks. Instead of writing the same code over and over, you write it once in a function and call it whenever you need it.
Think of functions like recipes:
- A recipe has a name ("Chocolate Cake")
- It takes ingredients (parameters)
- It has steps (the function body)
- It produces something (return value)
In TypeScript, functions are even more powerful because they have type safety . You can specify what types of inputs a function accepts and what type of output it produces. This prevents many common bugs!
Why Functions Matter
Without functions (repetitive code):
let total1 = 10 + 5 + 3;
console.log(`Cart 1 total: ${total1}`);
let total2 = 20 + 8 + 12;
console.log(`Cart 2 total: ${total2}`);
let total3 = 15 + 7 + 9;
console.log(`Cart 3 total: ${total3}`);
With functions (reusable code):
function calculateTotal(item1: number, item2: number, item3: number): number {
return item1 + item2 + item3;
}
console.log(`Cart 1 total: ${calculateTotal(10, 5, 3)}`);
console.log(`Cart 2 total: ${calculateTotal(20, 8, 12)}`);
console.log(`Cart 3 total: ${calculateTotal(15, 7, 9)}`);
TypeScript's Type Safety for Functions
The key difference in TypeScript is that you can (and should) specify types for parameters and return values:
JavaScript (no types):
function add(a, b) {
return a + b;
}
add(5, "10"); // "510" - string concatenation! Bug hiding in plain sight
TypeScript (with types):
function add(a: number, b: number): number {
return a + b;
}
add(5, 10); // ✅ 15
// add(5, "10"); // ❌ Error: Argument of type 'string' is not assignable to parameter of type 'number'
TypeScript catches the error before you run the code!
Function Declarations
The most straightforward way to create a function is with a function declaration .
Basic Syntax
function functionName(param1: type, param2: type): returnType {
// Function body
return value;
}
Examples
// Function that adds two numbers
function add(a: number, b: number): number {
return a + b;
}
console.log(add(5, 3)); // 8
// Function that greets someone
function greet(name: string): string {
return `Hello, ${name}!`;
}
console.log(greet("Alice")); // "Hello, Alice!"
// Function with no return value (void)
function logMessage(message: string): void {
console.log(message);
// No return statement needed
}
logMessage("This is a message");
// Function with multiple parameters
function calculateArea(width: number, height: number): number {
return width * height;
}
console.log(calculateArea(10, 5)); // 50
Understanding void
When a function doesn't return anything, its return type is void:
function printNumbers(count: number): void {
for (let i = 1; i <= count; i++) {
console.log(i);
}
// No return statement
}
printNumbers(5); // Prints 1, 2, 3, 4, 5
Hoisting
Function declarations are hoisted , meaning you can call them before they're defined in your code:
// This works!
console.log(sayHello("Alice")); // "Hello, Alice!"
function sayHello(name: string): string {
return `Hello, ${name}!`;
}
TypeScript (and JavaScript) moves function declarations to the top during compilation, so they're available everywhere in their scope.
Function Parameters
Parameters are the inputs to your function. TypeScript lets you make them required, optional, or have default values.
Required Parameters
By default, all parameters are required:
function divide(a: number, b: number): number {
return a / b;
}
divide(10, 2); // ✅ 5
// divide(10); // ❌ Error: Expected 2 arguments, but got 1
Optional Parameters
Add ? after the parameter name to make it optional:
function greet(name: string, greeting?: string): string {
if (greeting) {
return `${greeting}, ${name}!`;
}
return `Hello, ${name}!`;
}
console.log(greet("Alice")); // "Hello, Alice!"
console.log(greet("Alice", "Good morning")); // "Good morning, Alice!"
Important: Optional parameters must come after required ones:
// ✅ Correct
function build(required: string, optional?: number): void {}
// ❌ Wrong - required parameter after optional
// function bad(optional?: number, required: string): void {}
Default Parameters
Give parameters default values if not provided:
function createUser(name: string, age: number = 18, country: string = "USA"): string {
return `${name}, ${age}, from ${country}`;
}
console.log(createUser("Alice")); // "Alice, 18, from USA"
console.log(createUser("Bob", 25)); // "Bob, 25, from USA"
console.log(createUser("Charlie", 30, "Canada")); // "Charlie, 30, from Canada"
Default parameters are implicitly optional, so you don't need ?:
// This is redundant:
// function greet(name: string = "Guest"): string { ... }
// Just do this:
function greet(name: string = "Guest"): string {
return `Hello, ${name}!`;
}
Rest Parameters
Collect any number of arguments into an array:
function sum(...numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(5, 10, 15, 20)); // 50
console.log(sum(1, 2, 3, 4, 5, 6)); // 21
// Rest parameter must be last:
function logInfo(message: string, ...details: string[]): void {
console.log(message);
details.forEach(detail => console.log(`- ${detail}`));
}
logInfo("User created", "Name: Alice", "Age: 25", "Role: Admin");
Return Types
TypeScript can usually infer return types, but it's often good practice to specify them explicitly.
Explicit Return Types
// TypeScript infers return type as number
function add(a: number, b: number) {
return a + b;
}
// Explicit return type (recommended for clarity)
function subtract(a: number, b: number): number {
return a - b;
}
Multiple Return Points
All return statements must match the declared return type:
function getDiscount(age: number): number {
if (age < 18) {
return 10; // 10% youth discount
} else if (age >= 65) {
return 15; // 15% senior discount
}
return 0; // No discount
}
// This would error:
/*
function bad(age: number): number {
if (age < 18) {
return "child"; // ❌ Error: Type 'string' is not assignable to type 'number'
}
return 0;
}
*/
Union Return Types
Functions can return multiple types:
function findUser(id: number): string | null {
if (id > 0) {
return "User found";
}
return null; // User not found
}
const result = findUser(5);
console.log(result); // "User found"
const notFound = findUser(-1);
console.log(notFound); // null
Never Type
For functions that never return (they throw or have infinite loops):
function throwError(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {
// Never exits
}
}
Function Expressions
Functions can be assigned to variables (function expressions):
const add = function(a: number, b: number): number {
return a + b;
};
console.log(add(5, 3)); // 8
Anonymous Functions
Function expressions don't need a name:
const greet = function(name: string): string {
return `Hello, ${name}!`;
};
// Used in callbacks:
setTimeout(function() {
console.log("This runs after 1 second");
}, 1000);
Type Inference with Function Expressions
TypeScript can infer types based on assignment:
// Type is inferred from context
const numbers: number[] = [1, 2, 3, 4, 5];
const doubled = numbers.map(function(num) { // num is inferred as number
return num * 2;
});
Declaration vs Expression
Key difference: Function declarations are hoisted, expressions are not:
// ✅ This works - declaration is hoisted
console.log(declared(5));
function declared(x: number): number {
return x * 2;
}
// ❌ This errors - expression not hoisted
// console.log(expressed(5)); // Cannot access 'expressed' before initialization
const expressed = function(x: number): number {
return x * 2;
};
console.log(expressed(5)); // ✅ Works after definition
Arrow Functions
Arrow functions provide a shorter syntax (introduced in ES6):
Basic Syntax
// Traditional function
const add1 = function(a: number, b: number): number {
return a + b;
};
// Arrow function
const add2 = (a: number, b: number): number => {
return a + b;
};
// Concise arrow (implicit return)
const add3 = (a: number, b: number): number => a + b;
When to Use Each Style
// Single parameter - parentheses optional
const double = (x: number): number => x * 2;
// Multiple parameters - parentheses required
const multiply = (a: number, b: number): number => a * b;
// No parameters - empty parentheses required
const getRandomNumber = (): number => Math.random();
// Multi-line body - needs braces and explicit return
const calculateTotal = (price: number, tax: number): number => {
const subtotal = price;
const taxAmount = subtotal * tax;
return subtotal + taxAmount;
};
// Returning an object - wrap in parentheses
const createPerson = (name: string, age: number): { name: string; age: number } => ({
name,
age
});
Arrow Functions in Array Methods
Arrow functions shine with array methods:
const numbers: number[] = [1, 2, 3, 4, 5];
// map
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// filter
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4]
// reduce
const sum = numbers.reduce((total, n) => total + n, 0);
console.log(sum); // 15
// Chaining
const result = numbers
.filter(n => n > 2)
.map(n => n * 2)
.reduce((sum, n) => sum + n, 0);
console.log(result); // 24 (3*2 + 4*2 + 5*2 = 6 + 8 + 10)
Function Type Annotations
You can create types for functions themselves:
Function Type Syntax
// Define a function type
type MathOperation = (a: number, b: number) => number;
// Use it
const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;
const multiply: MathOperation = (a, b) => a * b;
console.log(add(5, 3)); // 8
console.log(subtract(10, 4)); // 6
console.log(multiply(3, 7)); // 21
Callback Types
Very useful for callbacks:
type Callback = (result: string) => void;
function processData(data: string, callback: Callback): void {
const processed = data.toUpperCase();
callback(processed);
}
processData("hello", (result) => {
console.log(result); // "HELLO"
});
Generic Function Types
We'll cover generics more later, but here's a preview:
type Transform<T> = (value: T) => T;
const double: Transform<number> = (x) => x * 2;
const shout: Transform<string> = (s) => s.toUpperCase();
console.log(double(5)); // 10
console.log(shout("hello")); // "HELLO"
Function Overloading
TypeScript allows multiple function signatures for the same function:
// Overload signatures
function combine(a: string, b: string): string;
function combine(a: number, b: number): number;
// Implementation signature
function combine(a: string | number, b: string | number): string | number {
if (typeof a === "string" && typeof b === "string") {
return a + b; // String concatenation
} else if (typeof a === "number" && typeof b === "number") {
return a + b; // Addition
}
throw new Error("Invalid arguments");
}
console.log(combine("Hello", "World")); // "HelloWorld"
console.log(combine(5, 10)); // 15
// console.log(combine("5", 10)); // ❌ Error: No overload matches
Another example:
function getValue(id: number): string;
function getValue(id: string): number;
function getValue(id: number | string): string | number {
if (typeof id === "number") {
return "Value for ID: " + id;
} else {
return id.length;
}
}
console.log(getValue(123)); // "Value for ID: 123"
console.log(getValue("hello")); // 5
Higher-Order Functions
Functions that take functions as parameters or return functions:
Functions as Parameters
function applyOperation(x: number, y: number, operation: (a: number, b: number) => number): number {
return operation(x, y);
}
const add = (a: number, b: number): number => a + b;
const multiply = (a: number, b: number): number => a * b;
console.log(applyOperation(5, 3, add)); // 8
console.log(applyOperation(5, 3, multiply)); // 15
Functions Returning Functions
function createMultiplier(factor: number): (n: number) => number {
return (n: number) => n * factor;
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
Real-World Example: Currying
function createGreeting(greeting: string): (name: string) => string {
return (name: string) => `${greeting}, ${name}!`;
}
const sayHello = createGreeting("Hello");
const sayGoodbye = createGreeting("Goodbye");
console.log(sayHello("Alice")); // "Hello, Alice!"
console.log(sayGoodbye("Bob")); // "Goodbye, Bob!"
Recursion
Functions that call themselves:
function factorial(n: number): number {
// Base case
if (n <= 1) {
return 1;
}
// Recursive case
return n * factorial(n - 1);
}
console.log(factorial(5)); // 120 (5 * 4 * 3 * 2 * 1)
// Countdown example
function countdown(n: number): void {
if (n < 0) {
return; // Base case
}
console.log(n);
countdown(n - 1); // Recursive call
}
countdown(5); // Prints: 5, 4, 3, 2, 1, 0
Recursive Type Safety
// Sum an array recursively
function sumArray(arr: number[]): number {
if (arr.length === 0) {
return 0;
}
return arr[0] + sumArray(arr.slice(1));
}
console.log(sumArray([1, 2, 3, 4, 5])); // 15
Concepts
Quick Reference
// Function declaration
function add(a: number, b: number): number {
return a + b;
}
// Function expression
const subtract = function(a: number, b: number): number {
return a - b;
};
// Arrow function
const multiply = (a: number, b: number): number => a * b;
// Optional parameter
function greet(name: string, greeting?: string): string {
return `${greeting ?? "Hello"}, ${name}!`;
}
// Default parameter
function createUser(name: string, age: number = 18): string {
return `${name}, age ${age}`;
}
// Rest parameters
function sum(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}
// Function type
type Operation = (a: number, b: number) => number;
// Void return
function log(message: string): void {
console.log(message);
}
// Union return
function findItem(id: number): string | null {
return id > 0 ? "Found" : null;
}
Examples
// Calculate discount
function calculateDiscount(price: number, percent: number = 10): number {
return price * (percent / 100);
}
console.log(calculateDiscount(100)); // 10
console.log(calculateDiscount(100, 20)); // 20
// Validate email
function isValidEmail(email: string): boolean {
return email.includes("@") && email.includes(".");
}
console.log(isValidEmail("user@email.com")); // true
console.log(isValidEmail("invalid")); // false
// Format name
function formatName(first: string, last: string, middle?: string): string {
if (middle) {
return `${first} ${middle} ${last}`;
}
return `${first} ${last}`;
}
console.log(formatName("John", "Doe")); // "John Doe"
console.log(formatName("John", "Doe", "Michael")); // "John Michael Doe"
// Process array
function processNumbers(numbers: number[], operation: (n: number) => number): number[] {
return numbers.map(operation);
}
const nums = [1, 2, 3, 4, 5];
console.log(processNumbers(nums, n => n * 2)); // [2, 4, 6, 8, 10]
console.log(processNumbers(nums, n => n ** 2)); // [1, 4, 9, 16, 25]
Challenges
1. Maximum of Two Numbers
- Write a function
maxthat takes two numbers and returns the larger one. - Add proper type annotations.
- Test with various number pairs.
💡 Show Answer
function max(a: number, b: number): number {
return a > b ? a : b;
}
console.log(max(5, 12)); // 12
console.log(max(100, 50)); // 100
console.log(max(-5, -10)); // -5
console.log(max(7, 7)); // 7
// Alternative implementation
function maxAlt(a: number, b: number): number {
if (a > b) {
return a;
}
return b;
}
// Using Math.max
function maxWithMath(a: number, b: number): number {
return Math.max(a, b);
}
// Generic version for any number of arguments
function maxOfAll(...numbers: number[]): number {
if (numbers.length === 0) {
throw new Error("At least one number required");
}
return Math.max(...numbers);
}
console.log(maxOfAll(3, 7, 2, 9, 1)); // 9
2. Even Number Checker (Arrow Function)
- Create an arrow function
isEventhat returns true if a number is even. - Use the concise syntax (implicit return).
- Test with even and odd numbers.
💡 Show Answer
const isEven = (n: number): boolean => n % 2 === 0;
console.log(isEven(4)); // true
console.log(isEven(7)); // false
console.log(isEven(0)); // true
console.log(isEven(-2)); // true
// Companion function
const isOdd = (n: number): boolean => n % 2 !== 0;
console.log(isOdd(4)); // false
console.log(isOdd(7)); // true
// Filter array usage
const numbers: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evens = numbers.filter(isEven);
const odds = numbers.filter(isOdd);
console.log(evens); // [2, 4, 6, 8, 10]
console.log(odds); // [1, 3, 5, 7, 9]
// Check if all are even
const allEven = (nums: number[]): boolean => nums.every(isEven);
console.log(allEven([2, 4, 6])); // true
console.log(allEven([2, 3, 6])); // false
3. String Reverser
- Write a function
reverseStringthat reverses a string. - Type the parameter and return value.
- Test with different strings.
💡 Show Answer
function reverseString(str: string): string {
return str.split("").reverse().join("");
}
console.log(reverseString("hello")); // "olleh"
console.log(reverseString("TypeScript")); // "tpircSepyT"
console.log(reverseString("racecar")); // "racecar" (palindrome)
// Check if palindrome using reverse
function isPalindrome(str: string): boolean {
const cleaned = str.toLowerCase().replace(/[^a-z0-9]/g, "");
return cleaned === reverseString(cleaned);
}
console.log(isPalindrome("racecar")); // true
console.log(isPalindrome("hello")); // false
console.log(isPalindrome("A man a plan a canal Panama")); // true
// Reverse words in a sentence
function reverseWords(sentence: string): string {
return sentence.split(" ").reverse().join(" ");
}
console.log(reverseWords("Hello World")); // "World Hello"
// Reverse each word individually
function reverseEachWord(sentence: string): string {
return sentence
.split(" ")
.map(word => reverseString(word))
.join(" ");
}
console.log(reverseEachWord("Hello World")); // "olleH dlroW"
4. Greeting with Default Parameter
- Create a function
greetUserwith a default parameter of "Guest". - Return a greeting message.
- Test with and without providing a name.
💡 Show Answer
function greetUser(name: string = "Guest"): string {
return `Hello, ${name}!`;
}
console.log(greetUser()); // "Hello, Guest!"
console.log(greetUser("Alice")); // "Hello, Alice!"
// With time of day
function greetUserWithTime(
name: string = "Guest",
timeOfDay: string = "day"
): string {
const greetings: { [key: string]: string } = {
morning: "Good morning",
afternoon: "Good afternoon",
evening: "Good evening",
day: "Hello"
};
const greeting = greetings[timeOfDay] || "Hello";
return `${greeting}, ${name}!`;
}
console.log(greetUserWithTime()); // "Hello, Guest!"
console.log(greetUserWithTime("Alice", "morning")); // "Good morning, Alice!"
console.log(greetUserWithTime("Bob", "evening")); // "Good evening, Bob!"
// With formal option
function greetUserFormal(
name: string = "Guest",
formal: boolean = false
): string {
if (formal) {
return `Good day, ${name}. How may I assist you?`;
}
return `Hey ${name}!`;
}
console.log(greetUserFormal("Alice")); // "Hey Alice!"
console.log(greetUserFormal("Alice", true)); // "Good day, Alice. How may I assist you?"
5. Array Summer
- Write a function
sumArraythat takes an array of numbers. - Return the sum of all elements.
- Use the
reducemethod.
💡 Show Answer
function sumArray(numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sumArray([1, 2, 3, 4, 5])); // 15
console.log(sumArray([10, 20, 30])); // 60
console.log(sumArray([])); // 0 (empty array)
console.log(sumArray([-5, 5])); // 0
// Calculate average
function averageArray(numbers: number[]): number {
if (numbers.length === 0) {
return 0;
}
return sumArray(numbers) / numbers.length;
}
console.log(averageArray([1, 2, 3, 4, 5])); // 3
// Sum only even numbers
function sumEvenNumbers(numbers: number[]): number {
return numbers
.filter(n => n % 2 === 0)
.reduce((total, num) => total + num, 0);
}
console.log(sumEvenNumbers([1, 2, 3, 4, 5, 6])); // 12 (2 + 4 + 6)
// Using rest parameters
function sum(...numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
console.log(sum(10, 20)); // 30
6. Function Declaration vs Expression
- Create both a function declaration and a function expression.
- Try calling both before and after their definitions.
- Observe and explain the hoisting behavior.
💡 Show Answer
// ✅ This works - declaration is hoisted
console.log(declared(5)); // 10
function declared(x: number): number {
return x * 2;
}
// ❌ This would error - expression not hoisted
// console.log(expressed(5)); // Error: Cannot access 'expressed' before initialization
const expressed = function(x: number): number {
return x * 2;
};
console.log(expressed(5)); // ✅ Works after definition: 10
// Arrow function (also not hoisted)
// console.log(arrowed(5)); // Error
const arrowed = (x: number): number => x * 2;
console.log(arrowed(5)); // ✅ 10
// Practical difference
function earlyBird(): void {
console.log("I can be called early!");
}
earlyBird(); // Works anywhere
// Best practice: Define before use for clarity
const lateBird = (): void => {
console.log("Call me after I'm defined");
};
lateBird(); // Works after definition
7. Callback with setTimeout
- Use an anonymous function in
setTimeout. - Print "Hello World" after 2 seconds.
- Try with both traditional function and arrow function syntax.
💡 Show Answer
// Traditional anonymous function
setTimeout(function() {
console.log("Hello World (traditional)");
}, 2000);
// Arrow function (more modern)
setTimeout(() => {
console.log("Hello World (arrow)");
}, 2000);
// With named function
function greet(): void {
console.log("Hello World (named)");
}
setTimeout(greet, 2000);
// With parameters
setTimeout((name: string) => {
console.log(`Hello, ${name}!`);
}, 2000, "Alice");
// Multiple delays
function countdown(n: number): void {
if (n <= 0) {
console.log("Done!");
return;
}
console.log(n);
setTimeout(() => countdown(n - 1), 1000);
}
countdown(3); // Prints: 3, 2, 1, Done! (with delays)
// Chaining timeouts
setTimeout(() => {
console.log("First");
setTimeout(() => {
console.log("Second");
setTimeout(() => {
console.log("Third");
}, 1000);
}, 1000);
}, 1000);
8. Function Transformer
- Write a function
applyTwicethat takes a function and a value. - Apply the function to the value twice.
- Test with different transformation functions.
💡 Show Answer
function applyTwice(fn: (n: number) => number, value: number): number {
return fn(fn(value));
}
function double(n: number): number {
return n * 2;
}
function addOne(n: number): number {
return n + 1;
}
function square(n: number): number {
return n * n;
}
console.log(applyTwice(double, 5)); // 20 (5 * 2 = 10, 10 * 2 = 20)
console.log(applyTwice(addOne, 5)); // 7 (5 + 1 = 6, 6 + 1 = 7)
console.log(applyTwice(square, 2)); // 16 (2² = 4, 4² = 16)
// Generic version for any type
function applyTwiceGeneric<T>(fn: (value: T) => T, value: T): T {
return fn(fn(value));
}
console.log(applyTwiceGeneric(double, 5)); // 20
console.log(applyTwiceGeneric((s: string) => s.toUpperCase(), "hello")); // "HELLO"
// Apply N times
function applyN(fn: (n: number) => number, value: number, times: number): number {
let result = value;
for (let i = 0; i < times; i++) {
result = fn(result);
}
return result;
}
console.log(applyN(double, 2, 3)); // 16 (2 * 2 * 2 * 2)
console.log(applyN(addOne, 0, 5)); // 5
9. Recursive Factorial
- Create a recursive function
factorial. - Calculate factorial of n (n! = n × (n-1) × ... × 1).
- Include a base case to prevent infinite recursion.
💡 Show Answer
function factorial(n: number): number {
// Base case
if (n <= 1) {
return 1;
}
// Recursive case
return n * factorial(n - 1);
}
console.log(factorial(5)); // 120 (5 * 4 * 3 * 2 * 1)
console.log(factorial(0)); // 1 (by definition)
console.log(factorial(10)); // 3628800
// Step by step for factorial(5):
// factorial(5) = 5 * factorial(4)
// factorial(4) = 4 * factorial(3)
// factorial(3) = 3 * factorial(2)
// factorial(2) = 2 * factorial(1)
// factorial(1) = 1 (base case)
// Working back: 2 * 1 = 2, 3 * 2 = 6, 4 * 6 = 24, 5 * 24 = 120
// With validation
function factorialSafe(n: number): number {
if (n < 0) {
throw new Error("Factorial not defined for negative numbers");
}
if (!Number.isInteger(n)) {
throw new Error("Factorial only defined for integers");
}
if (n <= 1) {
return 1;
}
return n * factorialSafe(n - 1);
}
// Iterative version (more efficient)
function factorialIterative(n: number): number {
let result = 1;
for (let i = 2; i <= n; i++) {
result *= i;
}
return result;
}
console.log(factorialIterative(5)); // 120
// Memoized version (caching results)
const factorialMemo = (() => {
const cache: { [key: number]: number } = {};
return function(n: number): number {
if (n in cache) {
return cache[n];
}
if (n <= 1) {
return 1;
}
cache[n] = n * factorialMemo(n - 1);
return cache[n];
};
})();
console.log(factorialMemo(5)); // 120 (calculated)
console.log(factorialMemo(5)); // 120 (from cache)
10. Function Factory (Closure)
- Write a function
makeMultiplierthat returns a new function. - The returned function should multiply its input by a factor.
- Create several multiplier functions with different factors.
💡 Show Answer
function makeMultiplier(factor: number): (n: number) => number {
return (n: number) => n * factor;
}
const double = makeMultiplier(2);
const triple = makeMultiplier(3);
const tenTimes = makeMultiplier(10);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(tenTimes(5)); // 50
// How closures work:
// The returned function "remembers" the factor from when it was created
// This is called a closure - the inner function has access to the outer function's variables
// More complex factory
function makeCalculator(operation: string): (a: number, b: number) => number {
if (operation === "add") {
return (a, b) => a + b;
} else if (operation === "subtract") {
return (a, b) => a - b;
} else if (operation === "multiply") {
return (a, b) => a * b;
} else if (operation === "divide") {
return (a, b) => a / b;
}
throw new Error("Unknown operation");
}
const add = makeCalculator("add");
const multiply = makeCalculator("multiply");
console.log(add(5, 3)); // 8
console.log(multiply(5, 3)); // 15
// Counter factory
function makeCounter(start: number = 0): () => number {
let count = start;
return () => {
count++;
return count;
};
}
const counter1 = makeCounter(0);
const counter2 = makeCounter(100);
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 101
console.log(counter1()); // 3
// Greeting factory
function makeGreeter(greeting: string): (name: string) => string {
return (name: string) => `${greeting}, ${name}!`;
}
const sayHello = makeGreeter("Hello");
const sayHi = makeGreeter("Hi");
const sayHowdy = makeGreeter("Howdy");
console.log(sayHello("Alice")); // "Hello, Alice!"
console.log(sayHi("Bob")); // "Hi, Bob!"
console.log(sayHowdy("Charlie")); // "Howdy, Charlie!"
Summary
In this chapter, you learned:
✅ How to create functions with proper TypeScript type annotations
✅ Different ways to define functions (declarations, expressions, arrows)
✅ Required, optional, and default parameters
✅ Rest parameters for variable-length arguments
✅ Return type annotations and type inference
✅ Function overloading for multiple signatures
✅ Higher-order functions and callbacks
✅ Recursion for self-referencing functions
✅ Closures and function factories
Key Takeaways:
- Always specify parameter and return types for clarity and safety
- Use arrow functions for concise, modern syntax (especially in callbacks)
- Default parameters provide cleaner code than checking for undefined
- Function declarations are hoisted, expressions are not
- Higher-order functions enable powerful functional programming patterns
- Recursion needs a base case to avoid infinite loops
- Closures allow functions to "remember" their creation environment
Best Practices:
- Be explicit with types even when TypeScript can infer them
- Use
constfor function expressions and arrow functions - Prefer arrow functions in callbacks for cleaner code
- Use descriptive function names that explain what they do
- Keep functions focused on a single task (Single Responsibility Principle)
- Use optional parameters sparingly - too many make functions hard to use
In the next chapter, we'll explore Strings and learn how to manipulate text with TypeScript's type-safe string methods!
Continue to Chapter 5: Strings →