Why TypeScript is a better option than JavaScript when it comes to functional programming?

remojansen

Remo H. Jansen

Posted on January 30, 2019

Why TypeScript is a better option than JavaScript when it comes to functional programming?

In this post, I would like to discuss the importance of static types in functional programming languages and why TypeScript is a better option than JavaScript when it comes to functional programming due to the lack of a static type system in JavaScript.

drawing

Life without types in a functional programming code base

Please try to put your mind on a hypothetical situation so we can showcase the value of static types. Let's imagine that you are writing some code for an elections-related application. You just joined the team, and the application is quite big. You need to write a new feature, and one of the requirements is to ensure that the user of the application is eligible to vote in the elections. One of the older members of the team has pointed out to us that some of the code that we need is already implemented in a module named @domain/elections and that we can import it as follows:

import { isEligibleToVote } from "@domain/elections";
Enter fullscreen mode Exit fullscreen mode

The import is a great starting point, and We feel grateful for the help provided by or workmate. It is time to get some work done. However, we have a problem. We don't know how to use isEligibleToVote. If we try to guess the type of isEligibleToVote by its name, we could assume that it is most likely a function, but we don't know what arguments should be provided to it:

isEligibleToVote(????);
Enter fullscreen mode Exit fullscreen mode

We are not afraid about reading someoneelses code do we open the source code of the source code of the @domain/elections module and we encounter the following:

const either = (f, g) => arg => f(arg) || g(arg);
const both = (f, g) => arg => f(arg) && g(arg);
const OUR_COUNTRY = "Ireland";
const wasBornInCountry = person => person.birthCountry === OUR_COUNTRY;
const wasNaturalized = person => Boolean(person.naturalizationDate);
const isOver18 = person => person.age >= 18;
const isCitizen = either(wasBornInCountry, wasNaturalized);
export const isEligibleToVote = both(isOver18, isCitizen);
Enter fullscreen mode Exit fullscreen mode

The preceding code snippet uses a functional programming style. The isEligibleToVote performs a series of checks:

  • The person must be over 10
  • The person must be a citizen
  • To be a citizen, the person must be born in the country or naturalized

We need to start doing some reverse engineering in our brain to be able to decode the preceding code. I was almost sure that isEligibleToVote is a function, but now I have some doubts because I don't see the function keyword or arrow functions (=>) in its declaration:

const isEligibleToVote = both(isOver18, isCitizen);
Enter fullscreen mode Exit fullscreen mode

TO be able to know what is it we need to examine what is the both function doing. I can see that both takes two arguments f and g and I can see that they are function because they are invoked f(arg) and g(arg). The both function returns a function arg => f(arg) && g(arg) that takes an argument named args and its shape is totally unknown for us at this point:

const both = (f, g) => arg => f(arg) && g(arg);
Enter fullscreen mode Exit fullscreen mode

Now we can return to the isEligibleToVote function and try to examine again to see if we can find something new. We now know that isEligibleToVote is the function returned by the both function arg => f(arg) && g(arg) and we also know that f is isOver18 and g is isCitizen so isEligibleToVote is doing something similar to the following:

const isEligibleToVote = arg => isOver18(arg) && isCitizen(arg);
Enter fullscreen mode Exit fullscreen mode

We still need to find out what is the argument arg. We can examine the isOver18 and isCitizen functions to find some details.

const isOver18 = person => person.age >= 18;
Enter fullscreen mode Exit fullscreen mode

This piece of information is instrumental. Now we know that isOver18 expects an argument named person and that it is an object with a property named age we can also guess by the comparison person.age >= 18 that age is a number.

Lets take a look to the isCitizen function as well:

const isCitizen = either(wasBornInCountry, wasNaturalized);

Enter fullscreen mode Exit fullscreen mode

We our out of luck here and we need to examine the either, wasBornInCountry and wasNaturalized functions:

const either = (f, g) => arg => f(arg) || g(arg);
const OUR_COUNTRY = "Ireland";
const wasBornInCountry = person => person.birthCountry === OUR_COUNTRY;
const wasNaturalized = person => Boolean(person.naturalizationDate);
Enter fullscreen mode Exit fullscreen mode

Both the wasBornInCountry and wasNaturalized expect an argument named person and now we have discovered new properties:

  • The birthCountry property seems to be a string
  • The naturalizationDate property seems to be date or null

The either function pass an argument to both wasBornInCountry and wasNaturalized which means that arg must be a person. It took a lot of cognitive effort, and we feel tired but now we know that we can use the isElegibleToVote function can be used as follows:

isEligibleToVote({
    age: 27,
    birthCountry: "Ireland",
    naturalizationDate: null
});

Enter fullscreen mode Exit fullscreen mode

We could overcome some of these problems using documentation such as JSDoc. However, that means more work and the documentation can get outdated quickly.

TypeScript can help to validate our JSDoc annotations are up to date with our code base. However, if we are going to do that, why not adopt TypeScript in the first place?

Life with types in a functional programming code base

Now that we know how difficult is to work in a functional programming code base without types we are going to take a look to how it feels like to work on a functional programming code base with static types. We are going to go back to the same starting point, we have joined a company, and one of our workmates has pointed us to the @domain/elections module. However, this time we are in a parallel universe and the code base is statically typed.

import { isEligibleToVote } from "@domain/elections";
Enter fullscreen mode Exit fullscreen mode

We don't know if isEligibleToVote is function. However, this time we can do much more than guessing. We can use our IDE to hover over the isEligibleToVote variable to confirm that it is a function:

We can then try to invoke the isEligibleToVote function, and our IDE will let us know that we need to pass an object of type Person as an argument:

If we try to pass an object literal our IDE will show as all the properties and of the Person type together with their types:

That's it! No thinking or documentation required! All thanks to the TypeScript type system.

The following code snippet contains the type-safe version of the @domain/elections module:

interface Person {
    birthCountry: string;
    naturalizationDate: Date | null;
    age: number;
}

const either = <T1>(
   f: (a: T1) => boolean,
   g: (a: T1) => boolean
) => (arg: T1) => f(arg) || g(arg);

const both = <T1>(
   f: (a: T1) => boolean,
   g: (a: T1) => boolean
) => (arg: T1) => f(arg) && g(arg);

const OUR_COUNTRY = "Ireland";
const wasBornInCountry = (person: Person) => person.birthCountry === OUR_COUNTRY;
const wasNaturalized = (person: Person) => Boolean(person.naturalizationDate);
const isOver18 = (person: Person) => person.age >= 18;
const isCitizen = either(wasBornInCountry, wasNaturalized);
export const isEligibleToVote = both(isOver18, isCitizen);
Enter fullscreen mode Exit fullscreen mode

Adding type annotations can take a little bit of additional type, but the benefits will undoubtedly pay off. Our code will be less prone to errors, it will be self-documented, and our team members will be much more productive because they will spend less time trying to understand the pre-existing code.

The universal UX principle Don't Make Me Think can also bring great improvements to our code. Remember that at the end of the day we spend much more time reading than writing code.

About types in functional programming languages

Functional programming languages don't have to be statically typed. However, functional programming languages tend to be statically typed. According to Wikipedia, this tendency has been rinsing since the 1970s:

Since the development of Hindley–Milner type inference in the 1970s, functional programming languages have tended to use typed lambda calculus, rejecting all invalid programs at compilation time and risking false positive errors, as opposed to the untyped lambda calculus, that accepts all valid programs at compilation time and risks false negative errors, used in Lisp and its variants (such as Scheme), though they reject all invalid programs at runtime, when the information is enough to not reject valid programs. The use of algebraic datatypes makes manipulation of complex data structures convenient; the presence of strong compile-time type checking makes programs more reliable in absence of other reliability techniques like test-driven development, while type inference frees the programmer from the need to manually declare types to the compiler in most cases.

Let's consider an object-oriented implementation of the isEligibleToVote feature without types:

const OUR_COUNTRY = "Ireland";

export class Person {
    constructor(birthCountry, age, naturalizationDate) {
        this._birthCountry = birthCountry;
        this._age = age;
        this._naturalizationDate = naturalizationDate;
    }
    _wasBornInCountry() {
        return this._birthCountry === OUR_COUNTRY;
    }
    _wasNaturalized() {
        return Boolean(this._naturalizationDate);
    }
    _isOver18() {
        return this._age >= 18;
    }
    _isCitizen() {
        return this._wasBornInCountry() || this._wasNaturalized();
    }
    isEligibleToVote() {
        return this._isOver18() && this._isCitizen();
    }
}
Enter fullscreen mode Exit fullscreen mode

Figuring this out how the preceding code should be invoked is not a trivial task:

import { Person } from "@domain/elections";

new Person("Ireland", 27, null).isEligibleToVote();
Enter fullscreen mode Exit fullscreen mode

Once more, without types, we are forced to take a look at the implementation details.

constructor(birthCountry, age, naturalizationDate) {
    this._birthCountry = birthCountry;
    this._age = age;
    this._naturalizationDate = naturalizationDate;
}
Enter fullscreen mode Exit fullscreen mode

When we use static types things become easier:

const OUR_COUNTRY = "Ireland";

class Person {

    private readonly _birthCountry: string;
    private readonly _naturalizationDate: Date | null;
    private readonly _age: number;

    public constructor(
        birthCountry: string,
        age: number,
        naturalizationDate: Date | null
    ) {
        this._birthCountry = birthCountry;
        this._age = age;
        this._naturalizationDate = naturalizationDate;
    }

    private _wasBornInCountry() {
        return this._birthCountry === OUR_COUNTRY;
    }

    private _wasNaturalized() {
        return Boolean(this._naturalizationDate);
    }

    private _isOver18() {
        return this._age >= 18;
    }

    private _isCitizen() {
        return this._wasBornInCountry() || this._wasNaturalized();
    }

    public isEligibleToVote() {
        return this._isOver18() && this._isCitizen();
    }

}
Enter fullscreen mode Exit fullscreen mode

The constructor tells us how many arguments are needed and the expected types of each of the arguments:

public constructor(
    birthCountry: string,
    age: number,
    naturalizationDate: Date | null
) {
    this._birthCountry = birthCountry;
    this._age = age;
    this._naturalizationDate = naturalizationDate;
}
Enter fullscreen mode Exit fullscreen mode

I personally think that functional programming is usually harder to reverse-engineering than object-oriented programming. Maybe this is due to my object-oriented background. However, whatever the reason I'm sure about one thing: Types really make my life easier, and their benefits are even more noticeable when I'm working on a functional programming code base.

Summary

Static types are a valuable source of information. Since we spend much more time reading code than writing code, we should optimize our workflow so we can be more efficient reading code rather than more efficient writing code. Types can help us to remove a great amount of cognitive effort so we can focus on the business problem that we are trying to solve.

While all of this is true in object-oriented programming code bases the benefits are even more noticeable in functional programming code bases and this exactly why I like to argue that TypeScript is a better option than JavaScript when it comes to functional programming. What do you think?

If you have enjoyed this post and you are interested in Functional Programming or TypeScript, please check out my upcoming book Hands-On Functional Programming with TypeScript

💖 💪 🙅 🚩
remojansen
Remo H. Jansen

Posted on January 30, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related