In the preceding section, we learned that a pure function returns a value that can be computed using only the arguments passed to it. A pure function also avoids mutating its arguments or any other external variable that is not passed to the function as an argument. In FP terminology, it is common to say that a pure function is a function that has no side-effects, which means that, when we invoke a pure function, we can expect that the function is not going to interfere (through a state mutation) with any other component in our application.
Certain programming languages, such as Haskell, can ensure that an application is free of side-effects using their type system. TypeScript has fantastic interoperability with JavaScript, but the downside of this, compared to a more isolated language such as Haskell, is that the type system is not able to guarantee that our application is free from side-effects. However, we can use some FP techniques to improve the type safety of our TypeScript applications. Let's take a look at an example:
interface User {
ageInMonths: number;
name: string;
}
function findUserAgeByName(users: User[], name: string): number {
if (users.length == 0) {
throw new Error("There are no users!");
}
const user = users.find(u => u.name === name);
if (!user) {
throw new Error("User not found!");
} else {
return user.ageInMonths;
}
}
The preceding function returns a number. The code compiles without issues. The problem is that the function does not always return a number. As a result, we can consume the function as follows and our code will compile and throw an exception at runtime:
const users = [
{ ageInMonths: 1, name: "Remo" },
{ ageInMonths: 2, name: "Leo" }
];
// The variable userAge1 is as number
const userAge1 = findUserAgeByName(users, "Remo");
console.log('Remo is ${userAge1 / 12} years old!');
// The variable userAge2 is a number but the function throws!
const userAge2 = findUserAgeByName([], "Leo"); // Error
console.log('Leo is ${userAge2 / 12} years old!');
The following example showcases a new implementation of the preceding function. This time, instead of returning a number, we will explicitly return a promise. The promise forces us to then use the handler. This handler is only executed if the promise is fulfilled, which means that if the function returns an error, we will never try to convert the age to years:
function safeFindUserAgeByName(users: User[], name: string): Promise<number> {
if (users.length == 0) {
return Promise.reject(new Error("There are no users!"));
}
const user = users.find(u => u.name === name);
if (!user) {
return Promise.reject(new Error("User not found!"));
} else {
return Promise.resolve(user.ageInMonths);
}
}
safeFindUserAgeByName(users, "Remo")
.then(userAge1 => console.log('Remo is ${userAge1 / 12} years old!'));
safeFindUserAgeByName([], "Leo") // Error
.then(userAge1 => console.log('Leo is ${userAge1 / 12} years old!'));
The Promise type helps us to prevent errors because it expresses potential errors in an explicit way. In programming languages such as Haskell, this is the default behavior of the type system, but, in programming languages such as TypeScript, it is up to us to use types in a safer way.