Post

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. πŸš€

03. Functions in TypeScript

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 undefined if 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[] means items will be an array of strings, 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, identity simply returns whatever you give it.

    • If you call identity(123), T becomes number, and it returns a number.
    • If you call identity("hello"), T becomes string, and it returns a string. It perfectly maintains the original type!

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 πŸ“š

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 common this context issues. For complex functions, or methods that need their own dynamic this context, 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

  1. Create a generic Task<T> interface with id, title, and data of type T
  2. Implement an overloaded createTask function that accepts:
    • (title: string) β†’ creates a simple task
    • (title: string, data: T) β†’ creates a task with custom data
  3. Build a TaskManager class with methods:
    • addTask<T>(task: Task<T>): void - adds a task
    • getTasks(): Task<any>[] - returns all tasks
    • filterTasks(callback: (task: Task<any>) => boolean): Task<any>[] - filters tasks
  4. Use arrow functions for callbacks
  5. Include proper TypeScript type annotations throughout

πŸ’‘ Implementation Hints

  1. Start by defining the Task<T> interface with generic type parameter
  2. Create overload signatures before the implementation function
  3. Use rest parameters if you want to add multiple tasks at once
  4. Leverage arrow functions for filtering and mapping tasks
  5. 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 filterTasks to 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 sortTasks function 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.

This post is licensed under CC BY 4.0 by the author.