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?
- Modularity: Break down complex tasks into smaller, reusable pieces.
- Reusability: Define once, use multiple times.
- Maintainability: Easier to debug and update isolated pieces of logic.
- 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:
- Takes one or more functions as arguments.
- 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
mapfunction is a higher-order function because it takes a function(num: number) => num * numas 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:
Arrow function:
2. Implicit Return
If the arrow function body consists of a single expression, you can omit the braces {} and the return keyword.
3. Single Parameter Parentheses Omission
If there’s only one parameter, you can omit the parentheses around the parameter.
4. No Parameters
If there are no parameters, you must include an empty set of parentheses.
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.
6. Higher Order Functions
Arrow functions shine when used with higher-order functions like map, filter, and reduce.
Traditional function with map:
Using arrow function with map:
Points to Remember:
-
Arrow functions do not have their own
thisvalue. They inheritthisfrom the enclosing scope. This makes them unsuitable for methods in classes if you need to access class properties usingthis. -
Arrow functions cannot be used as constructors.
-
Arrow functions do not have the
argumentsobject. 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:
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.