Skip to content

Functions

In TypeScript, as in JavaScript, Functions are the primary units of work. They encapsulate a series of statements that perform a specific task. TypeScript brings additional features to functions, such as static typing for parameters and return values, making them more robust and predictable.

Why Use Functions?

  1. Modularity: Break down complex tasks into smaller, reusable pieces.
  2. Reusability: Define once, use multiple times.
  3. Maintainability: Easier to debug and update isolated pieces of logic.
  4. Abstraction: Hide complex implementation details, exposing only what’s necessary.

Function Types

In TypeScript, you can specify the types of parameters and the return value for a function:

function greet1(name: string): string {
    return "Hello, " + name;
}

const greet2 = function (name: string): string {
    return "Hello, " + name;
};

console.log(greet1("a"));
console.log(greet1("b"));

Optional and Default Parameters

TypeScript allows function parameters to be optional (using ?) or have default values:

function registerUser(
    username: string,
    isAdmin: boolean = false,
    email?: string
): void {
    console.log(username);
    console.log(isAdmin);
    console.log(email);
}

registerUser("a", true, "a@a.dk");  // a, true, a@a.dk
registerUser("b", true);            // b, true, undefined
registerUser("c");                  // c, false, undefined

Rest Parameters

Capture a variable number of arguments as an array:

function buildName(firstName: string, ...restOfName: string[]): string {
    return firstName + " " + restOfName.join(" ");
}
Higher-Order Functions in TypeScript

A higher-order function is a function that either:

  1. Takes one or more functions as arguments.
  2. Returns a function as its result.

Higher-order functions are a fundamental concept in functional programming, and they allow for operations like map, filter, and reduce, which can transform, filter, or accumulate data, respectively.

The ability to use functions as values, pass them as arguments, or return them from other functions allows for a high degree of flexibility and promotes a declarative approach to problem-solving.

Example: The map Function

One of the most common higher-order functions in TypeScript (and JavaScript) is the map function, available on arrays. The map function takes a function as an argument and applies this function to each element of the array, producing a new array with the transformed values.

Let’s see an example where we use the map function to square each number in an array:

// Define an array of numbers
const numbers: number[] = [1, 2, 3, 4, 5];

// Use the map function to square each number
const squaredNumbers: number[] = numbers.map((num: number) => num * num);

console.log(squaredNumbers); // Output: [1, 4, 9, 16, 25]

In this example:

  • The map function is a higher-order function because it takes a function (num: number) => num * num as an argument.
  • The provided function is an arrow function that squares its input.
  • The result is a new array where each number is squared.

Arrow Functions

Arrow functions, introduced in ES6 and available in TypeScript, allow for a shorter syntax when writing functions. There are several ways to simplify the code using arrow functions:

1. Basic Syntax

Traditional function:

function greet(name: string) {
    return "Hello, " + name;
}

Arrow function:

const greet = (name: string) => {
    return "Hello, " + name;
}

2. Implicit Return

If the arrow function body consists of a single expression, you can omit the braces {} and the return keyword.

const greet = (name: string) => "Hello, " + name;

3. Single Parameter Parentheses Omission

If there’s only one parameter, you can omit the parentheses around the parameter.

const greet = name => "Hello, " + name;

4. No Parameters

If there are no parameters, you must include an empty set of parentheses.

const greetWorld = () => "Hello, World!";

5. Object Literal Return

If you’re returning an object literal using the implicit return, wrap the object in parentheses to avoid confusion with the function block.

const createPerson = (name: string, age: number) => ({ name, age });

6. Higher Order Functions

Arrow functions shine when used with higher-order functions like map, filter, and reduce.

Traditional function with map:

const numbers = [1, 2, 3];
const doubled = numbers.map(function(number) {
    return number * 2;
});

Using arrow function with map:

const numbers = [1, 2, 3];
const doubled = numbers.map(number => number * 2);

Points to Remember:

  • Arrow functions do not have their own this value. They inherit this from the enclosing scope. This makes them unsuitable for methods in classes if you need to access class properties using this.

  • Arrow functions cannot be used as constructors.

  • Arrow functions do not have the arguments object. If you need to access arguments, consider using the rest parameters.

Arrow functions provide a concise way to write functions in TypeScript and JavaScript, making the code more readable, especially for functional programming patterns.

Function Overloads

TypeScript supports function overloading, allowing multiple function types for the same function, based on the arguments:

function add(a: string, b: string): string;
function add(a: number, b: number): number;

function add(a: any, b: any): any {
    if (typeof a === "string" && typeof b === "string") {
        return a + b;
    }
    if (typeof a === "number" && typeof b === "number") {
        return a + b;
    }
}

Callbacks and Higher-Order Functions

Functions can accept other functions as parameters or return functions:

function processItems(items: string[], callback: (item: string) => void): void {
    for (let item of items) {
        callback(item);
    }
}

Generics in Functions

Generics allow functions to work with any type while still maintaining type safety:

function identity<T>(arg: T): T {
    return arg;
}

Advanced Topics

Function Currying

Currying is a functional programming technique where a function with multiple arguments is transformed into a sequence of functions, each taking a single argument. This allows for partial application of functions.

// Regular function
function add(a: number, b: number, c: number): number {
    return a + b + c;
}

// Curried version
function addCurried(a: number) {
    return function(b: number) {
        return function(c: number) {
            return a + b + c;
        }
    }
}

// Arrow function syntax for currying
const addCurriedArrow = (a: number) => (b: number) => (c: number) => a + b + c;

console.log(add(1, 2, 3));              // 6
console.log(addCurried(1)(2)(3));       // 6
console.log(addCurriedArrow(1)(2)(3));  // 6

// Partial application
const add5 = addCurriedArrow(5);
const add5And10 = add5(10);
console.log(add5And10(20));  // 35

Function Composition

Function composition is the process of combining two or more functions to produce a new function. When you compose functions, the output of one function becomes the input of the next.

// Simple functions
const double = (x: number): number => x * 2;
const addTen = (x: number): number => x + 10;
const square = (x: number): number => x * x;

// Compose function (right to left)
function compose<T>(...fns: Array<(arg: T) => T>) {
    return (value: T): T => {
        return fns.reduceRight((acc, fn) => fn(acc), value);
    };
}

// Pipe function (left to right)
function pipe<T>(...fns: Array<(arg: T) => T>) {
    return (value: T): T => {
        return fns.reduce((acc, fn) => fn(acc), value);
    };
}

const composedFn = compose(addTen, double, square);
console.log(composedFn(3));  // 28 (square(3) = 9, double(9) = 18, addTen(18) = 28)

const pipedFn = pipe(square, double, addTen);
console.log(pipedFn(3));  // 28 (square(3) = 9, double(9) = 18, addTen(18) = 28)

Async Function Patterns

Modern TypeScript applications often deal with asynchronous operations. Here are some advanced patterns:

// Async function with error handling
async function fetchUserData(userId: string): Promise<{ name: string; email: string } | null> {
    try {
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) {
            throw new Error('User not found');
        }
        return await response.json();
    } catch (error) {
        console.error('Error fetching user:', error);
        return null;
    }
}

// Parallel execution with Promise.all
async function fetchMultipleUsers(userIds: string[]) {
    const promises = userIds.map(id => fetchUserData(id));
    const results = await Promise.all(promises);
    return results.filter((user): user is NonNullable<typeof user> => user !== null);
}

// Retry pattern
async function fetchWithRetry<T>(
    fn: () => Promise<T>,
    retries: number = 3,
    delay: number = 1000
): Promise<T> {
    try {
        return await fn();
    } catch (error) {
        if (retries <= 0) throw error;

        await new Promise(resolve => setTimeout(resolve, delay));
        return fetchWithRetry(fn, retries - 1, delay);
    }
}

In essence, functions in TypeScript are versatile and robust, allowing developers to write predictable and type-safe code. They form the backbone of any TypeScript application, from simple scripts to complex libraries and frameworks.