03. Functions in TypeScript
β¨ Elevate your coding with TypeScript functions! Discover how to build robust, type-safe logic using optional parameters, generics, overloading, and arrow functions for cleaner, more predictable applications. π
What we will learn in this post?
- π Typed Functions in TypeScript
- π Optional and Default Parameters
- π Rest Parameters and Spread Operator
- π Function Overloading
- π Generic Functions
- π Arrow Functions and This Context
- π Function Types and Callbacks
Typed Functions in TypeScript π―
What are Typed Functions? π€
In TypeScript, you can specify types for function parameters and return values. This helps catch errors early and makes your code more predictable.
Basic Function Syntax π
1
2
3
4
5
function greet(name: string): string {
return `Hello, ${name}!`;
}
const result = greet("Alice"); // result is a string
Parameter Types π
You must specify the type for each parameter:
1
2
3
function add(x: number, y: number): number {
return x + y;
}
Return Types π
The return type comes after the parameters:
1
2
3
function isAdult(age: number): boolean {
return age >= 18;
}
Void Functions π«
Functions that donβt return anything use void:
1
2
3
function logMessage(message: string): void {
console.log(message);
}
π More Info:
Optional and Default Parameters π¨
Optional Parameters β
Mark parameters as optional using ?. Optional parameters can be undefined:
1
2
3
4
5
6
7
8
9
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 π―
Provide default values for parameters:
1
2
3
4
5
6
function greet(name: string, greeting: string = "Hello"): string {
return `${greeting}, ${name}!`;
}
console.log(greet("Alice")); // "Hello, Alice!"
console.log(greet("Bob", "Hi")); // "Hi, Bob!"
Key Rules π
- Optional parameters must come after required parameters
- Default parameters donβt need to be at the end
- Optional parameters are automatically
undefinedif not provided
graph LR
A["π― Function Call"]:::style1 --> B{"β Has Optional Param?"}:::style3
B -- "Yes" --> C["β
Use Provided Value"]:::style4
B -- "No" --> D{"π Has Default Value?"}:::style3
D -- "Yes" --> E["βοΈ Use Default"]:::style2
D -- "No" --> F["β undefined"]:::style5
classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style4 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style5 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
linkStyle default stroke:#e67e22,stroke-width:3px;
β¨ Flexible Functions with Rest & Spread!
Letβs make our TypeScript functions super versatile! Weβll look at two powerful tools for handling arguments: Rest Parameters and the Spread Operator.
π¦ Rest Parameters: Gathering Arguments
Ever wanted a function to accept any number of arguments? Thatβs where Rest Parameters (...args: Type[]) come in! They let you collect multiple, individual inputs into a single array inside your function. Think of it as a βcollectorβ!
- Type Safety:
...items: string[]meansitemswill be an array ofstrings, giving you strong type checks. - Placement: A rest parameter must always be the very last parameter.
1
2
3
4
5
6
function combineWords(separator: string, ...words: string[]): string {
// 'words' is an array here, like ["hello", "world"]
return words.join(separator);
}
console.log(combineWords("-", "apple", "banana", "cherry"));
// Output: "apple-banana-cherry"
β¨ Spread Operator: Spreading Arguments
The Spread Operator (...array) does the opposite! It takes an array and βunpacksβ its elements, spreading them out as individual arguments. Think of it as an βunpackerβ for when a function expects separate values.
1
2
3
4
const fruitsToAdd = ["grape", "kiwi"];
console.log(combineWords(" + ", "orange", ...fruitsToAdd, "melon"));
// ...fruitsToAdd unpacks to "grape", "kiwi" as separate arguments.
// Output: "orange + grape + kiwi + melon"
π Why Are They So Cool?
They make your functions incredibly flexible and cleaner! Rest parameters handle varied input effortlessly, while the spread operator simplifies passing array elements where individual arguments are expected.
π‘ Real-World Example: Logger Function
1
2
3
4
5
6
7
8
9
// Production-ready logger with rest parameters
function log(level: string, timestamp: Date, ...messages: string[]): void {
const formattedTime = timestamp.toISOString();
console.log(`[${level}] ${formattedTime}:`, ...messages);
}
// Usage in web application
log("ERROR", new Date(), "Database connection failed", "Retrying...");
log("INFO", new Date(), "User logged in", "Session started");
graph TD
A["π Function Call"]:::style1 -->|"...args (Rest)"| B["π¦ Array inside Function"]:::style4
C["π Array"]:::style2 -->|"...array (Spread)"| D["β‘ Individual Arguments"]:::style5
classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style4 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style5 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
linkStyle default stroke:#e67e22,stroke-width:3px;
Function Overloading π
What is Function Overloading? π€
Function overloading lets you define multiple signatures for the same function. The function can accept different parameter types or counts.
Basic Syntax π
1
2
3
4
5
6
7
8
9
10
11
// Overload signatures
function combine(a: string, b: string): string;
function combine(a: number, b: number): number;
// Implementation signature
function combine(a: any, b: any): any {
return a + b;
}
console.log(combine("Hello", " World")); // "Hello World"
console.log(combine(10, 20)); // 30
Why Use Overloading? π‘
- Better type safety for different input combinations
- Clearer function documentation
- IDE provides better autocomplete
Key Points π―
- Write overload signatures first
- Implementation signature must be compatible with all overloads
- The implementation signature is not directly callable
graph TD
A["π― Function Call"]:::style1 --> B{"π Check Overload 1"}:::style3
B -- "β
Match" --> C["π Use Overload 1 Types"]:::style4
B -- "β No Match" --> D{"π Check Overload 2"}:::style3
D -- "β
Match" --> E["π Use Overload 2 Types"]:::style4
D -- "β No Match" --> F["β οΈ Type Error"]:::style5
C --> G["βοΈ Run Implementation"]:::style2
E --> G
classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style4 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style5 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
linkStyle default stroke:#e67e22,stroke-width:3px;
Understanding Generic Functions π
Generics are fantastic tools that let you write incredibly flexible and reusable functions! Imagine creating a single function that can gracefully handle any type of data β be it numbers, strings, or even complex objects β without you needing to write a separate, specialized version for each. Thatβs the magic of generics!
Why Generics are Awesome! π€
They help you build type-safe code that is also highly reusable. Instead of duplicating logic for different data types, generics provide one robust, clear solution, making your code cleaner and less error-prone.
The Magic of <T> β¨
The <T> you see in a function definition is a type parameter. Think of T as a clever placeholder for any type that will be passed into the function when itβs called.
- Practical Example: The Identity Function
1 2 3
function identity<T>(arg: T): T { return arg; }
Here,
identitysimply returns whatever you give it.- If you call
identity(123),Tbecomesnumber, and it returns anumber. - If you call
identity("hello"),Tbecomesstring, and it returns astring. It perfectly maintains the original type!
- If you call
Smarter Generics with Constraints π
Sometimes, T might need to have certain characteristics (e.g., it must be an object with a length property). Generic constraints (like T extends { length: number }) let you specify these requirements, ensuring type safety while maintaining flexibility.
Beyond One Type: Multiple Parameters π€
You can use multiple type parameters, such as <T, U>, when your function needs to work with several different, possibly related, types simultaneously. For instance, a generic function that processes two different types of inputs.
Preserving Type Information π‘οΈ
Generics truly shine by preserving type information. They tell TypeScript exactly what type is expected, allowing it to catch potential errors during development, rather than having issues pop up at runtime. This is significantly safer than using any!
graph TD
A["π― Call Generic Function"]:::style1 --> B["π₯ Input Data (Type: X)"]:::style4
B --> C["βοΈ Generic Function<T>"]:::style2
C --> D["π T takes on Type X"]:::style3
D --> E["π‘ Function Logic"]:::style2
E --> F["π€ Output Data (Type: X)"]:::style4
classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style4 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
linkStyle default stroke:#e67e22,stroke-width:3px;
π‘ Real-World Example: API Response Cache
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Generic cache function for API responses
function cacheResponse<T>(key: string, data: T): T {
localStorage.setItem(key, JSON.stringify(data));
return data;
}
function getCachedResponse<T>(key: string): T | null {
const cached = localStorage.getItem(key);
return cached ? JSON.parse(cached) : null;
}
// Usage with different data types
interface User { id: number; name: string; }
interface Product { sku: string; price: number; }
const user = cacheResponse<User>("user_123", { id: 123, name: "Alice" });
const product = cacheResponse<Product>("prod_456", { sku: "ABC123", price: 99.99 });
const cachedUser = getCachedResponse<User>("user_123"); // Returns User | null
Further Learning π
- For a deeper dive, check out the TypeScript Generics Official Documentation.
Arrow Functions in TypeScript: A Quick Guide π
What are Arrow Functions? π€
Arrow functions offer a concise way to write functions in TypeScript. Their syntax is typically (parameters) => expression for an implicit return (no return keyword needed if itβs a single expression), or (parameters) => { statements } for explicit returns with multiple lines of code. Theyβre shorter and often clearer than traditional function declarations, especially for simple tasks.
The Magic of this β¨
One major difference is how they handle the this keyword. Arrow functions lexically capture this from their surrounding scope. This means this inside an arrow function will always refer to the this of the code block where the arrow function was defined, making its context predictable. Regular functions, however, determine this based on how they are called, which can lead to unexpected behavior.
graph TD
A["π Function Definition"]:::style1 --> B{"β Is it an Arrow Function?"}:::style3
B -- "β
Yes" --> C["π Captures 'this' from surrounding scope"]:::style4
B -- "β No" --> D["π 'this' depends on how it's called"]:::style2
C --> E["β¨ Consistent 'this' context"]:::style4
D --> F["β‘ Dynamic 'this' context"]:::style5
classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style4 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style5 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
linkStyle default stroke:#e67e22,stroke-width:3px;
When to Use Them? β
Arrow functions shine in scenarios where you need a function that doesnβt rebind this, such as:
- Callbacks:
array.map(item => item * 2) - Event Handlers:
button.addEventListener('click', () => console.log(this.name))They prevent commonthiscontext issues. For complex functions, or methods that need their own dynamicthiscontext, regular functions are often more suitable.
Examples π‘
1
2
3
4
5
6
7
8
9
10
11
12
// Callback example (implicit return!)
const numbers = [1, 2, 3];
const doubled = numbers.map(num => num * 2); // doubled is [2, 4, 6]
// Event handler example (captures 'this' from class instance)
class Greeter {
message = "Hello!";
greet = () => console.log(this.message); // 'this' refers to the Greeter instance
}
const greeter = new Greeter();
// greeter.greet() will correctly log "Hello!"
Function Types and Callbacks π
What are Function Types? π
Function types describe the shape of a function, including its parameters and return type.
Declaring Function Types π
1
2
3
4
5
6
// Function type
type MathOperation = (a: number, b: number) => number;
// Using the type
const add: MathOperation = (x, y) => x + y;
const multiply: MathOperation = (x, y) => x * y;
Callback Functions π
Callbacks are functions passed as arguments to other functions:
1
2
3
4
5
6
7
function processData(data: string[], callback: (item: string) => void): void {
data.forEach(item => callback(item));
}
processData(["a", "b", "c"], (item) => {
console.log(item.toUpperCase());
});
Type-Safe Callbacks π‘οΈ
1
2
3
4
5
6
7
8
9
type Callback = (error: Error | null, result?: string) => void;
function fetchData(url: string, callback: Callback): void {
if (url) {
callback(null, "Data loaded");
} else {
callback(new Error("Invalid URL"));
}
}
Function Interface π―
1
2
3
4
5
6
interface Formatter {
(value: string): string;
}
const uppercase: Formatter = (val) => val.toUpperCase();
const lowercase: Formatter = (val) => val.toLowerCase();
graph LR
A["π― Higher-Order Function"]:::style1 --> B["π₯ Receives Callback"]:::style2
B --> C["βοΈ Executes Callback"]:::style4
C --> D["π€ Returns Result"]:::style5
classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style4 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style5 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
linkStyle default stroke:#e67e22,stroke-width:3px;
π‘ Real-World Example: Form Validation Pipeline
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Type-safe validation callback system
type ValidationCallback = (value: string) => boolean;
type ErrorCallback = (field: string, message: string) => void;
function validateForm(
fields: { [key: string]: string },
validators: { [key: string]: ValidationCallback },
onError: ErrorCallback
): boolean {
let isValid = true;
for (const field in fields) {
const value = fields[field];
const validator = validators[field];
if (validator && !validator(value)) {
onError(field, `Invalid ${field}`);
isValid = false;
}
}
return isValid;
}
// Usage in production form handling
const formData = {
email: "user@example.com",
password: "12345"
};
const validators = {
email: (val: string) => val.includes("@"),
password: (val: string) => val.length >= 8
};
validateForm(formData, validators, (field, msg) => {
console.error(`${field}: ${msg}`);
});
π More Info:
π― Hands-On Assignment: Build a Task Management System π
π Your Mission
Build a **Task Management System** using TypeScript functions that demonstrates typed functions, generics, callbacks, and function overloading.π― Requirements
- Create a generic
Task<T>interface with id, title, and data of type T - Implement an overloaded
createTaskfunction that accepts:(title: string)β creates a simple task(title: string, data: T)β creates a task with custom data
- Build a
TaskManagerclass with methods:addTask<T>(task: Task<T>): void- adds a taskgetTasks(): Task<any>[]- returns all tasksfilterTasks(callback: (task: Task<any>) => boolean): Task<any>[]- filters tasks
- Use arrow functions for callbacks
- Include proper TypeScript type annotations throughout
π‘ Implementation Hints
- Start by defining the
Task<T>interface with generic type parameter - Create overload signatures before the implementation function
- Use rest parameters if you want to add multiple tasks at once
- Leverage arrow functions for filtering and mapping tasks
- Add default parameters where appropriate
π Example Input/Output
interface TodoData {
completed: boolean;
priority: number;
}
const manager = new TaskManager();
// Using overloaded function
const simpleTask = createTask("Buy groceries");
const todoTask = createTask<TodoData>("Complete assignment", {
completed: false,
priority: 1
});
manager.addTask(simpleTask);
manager.addTask(todoTask);
// Using callback with arrow function
const incompleteTasks = manager.filterTasks(task =>
!task.data?.completed
);
console.log(incompleteTasks); // [{ id: 2, title: "Complete assignment", ... }]
π Bonus Challenges
- Level 2: Add a
mapTasks<T, U>method that transforms tasks using a callback - Level 3: Implement function overloading for
filterTasksto accept different filter types - Level 4: Use generic constraints to ensure certain task types have required properties
- Level 5: Add error handling with typed error callbacks
- Level 6: Implement a
sortTasksfunction with comparison callback
π Learning Goals
- Master typed function parameters and return types π―
- Practice generic type parameters with constraints β¨
- Implement function overloading patterns π
- Build type-safe callback systems π
- Create reusable TypeScript utilities π οΈ
π‘ Pro Tip: This pattern is used in task management libraries like TodoMVC, state management systems like Redux, and workflow engines!
Share Your Solution! π¬
Completed the project? Post your code in the comments below! Show us your TypeScript function mastery! πβ¨
Conclusion π
TypeScript functions elevate your code with type safety, making it more predictable and maintainable while catching errors during development. By mastering typed parameters, generics, overloading, and callbacks, youβll write robust applications that scale effortlessly from small scripts to enterprise systems.