loading...
Wolk Software

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

remojansen profile image Remo H. Jansen ・7 min read

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";

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(????);

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);

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);

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);

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);

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;

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);

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);

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
});

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";

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);

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();
    }
}

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();

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;
}

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();
    }

}

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;
}

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

Posted on by:

remojansen profile

Remo H. Jansen

@remojansen

TypeScript Microsoft MVP, writer, speaker technology-lover and OSS enthusiast. Author of Learning TypeScript by PacktPub, InversifyJS and ZafiroJS. Organizer of Dublin TypeScript and Dublin OSS.

Wolk Software

Wolk Software is a software consultancy and corporate training services company dedicated to empowering developers and teams to be their best.

Discussion

markdown guide
 

👍 Nice article with good examples based on a real and interesting domain.

💡 Examples can be more readable and TypeScript idiomatics this way (IMHO):

// -- Common ----

const IRELAND = Object.freeze({
    name: 'Ireland',
    ageOfMajority: 18,
});

// -- FP ----

// Helpers
type predicate<T> = (a: T) => boolean;

const either: <T>(f: predicate<T>, g: predicate<T>) => predicate<T> =
    (f, g) => a => f(a) || g(a);

const both: <T>(f: predicate<T>, g: predicate<T>) => predicate<T> =
    (f, g) => a => f(a) && g(a);

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

const wasBornInCountry = (person: Person) => person.birthCountry === IRELAND.name;
const wasNaturalized   = (person: Person) => Boolean(person.naturalizationDate);
const isOver18         = (person: Person) => person.age >= IRELAND.ageOfMajority;
const isCitizen        = either(wasBornInCountry, wasNaturalized);

export const isEligibleToVote = both(isOver18, isCitizen);

// -- OOP ----

class Person {
    constructor(
        // Prefer inline properties
        // Prefer no "_" prefix for private fields except to ease JavaScript initeractions
        private readonly _age: number,
        private readonly _birthCountry: string,
        private readonly _naturalizationDate: Date | null = null,
    ) { }

    // Prefer "step-down rule": outside-in declarations
    isEligibleToVote() {
        return this._isOver18() && this._isCitizen();
    }

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

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

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

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

A other solution is to structure isEligableToVote in a more procedural way so you, at a glaze, can see what happens and what parameters are expected.
Plus add some unit tests to describe and test common use cases. And volia, your need for Typescript is gone. Of course you can still add it for static typing, but the added benifits is not worth the extra code, typing and tooling you need to introduce :)

 

I never used TypeScript in production. So, I have no idea about TS benefits. I just used in personal projects. That does not give to me any idea about benefits.

On the other hand, we've many large applications written with Vanilla JavaScript.

For example, what should I do to see the benefits of TypeScript?

This is an important question for me and your answer will be important to understand somethings. Because I'll decide (I'm the only Front-End Developer at Work) to move our codebase to TypeScript.

Thanks :)

 

I looked at your website, and I see that you have a broad experience with different technologies, different languages, different programming paradigms, different frameworks, and different operating systems. A kindred spirit!

If you are still doing work on the .NET platform, I think you will enjoy The Book of F# by Dave Fancher, as an introductory and tutorial book. F# is basically OCaml for .NET, by Don Syme of Microsoft in Cambridge. (I'm just a fan of the book, no vested interest. Fancher's writing style was on my wavelength for how I learn. The other F# books I've read have been disappointing. Note: I've not read Expert F# by Don Syme yet; I've got it in my book queue.)

I've used TypeScript day in, and day out, for two years on a big project at a former company. For writing industrial strength JavaScript at scale, it is a better JavaScript than JavaScript. I look forward to reading your book. :-)

 

try composing identity function in TS. 🙁

 

When I write libraries meant to be imported in other projects I usually write type definition files for the public interface, since some typing is still better than no typing, but I do feel that TypeScript is considerably lacking when it comes to typing functional constructs. I wish that TypeScript had brought us algebraic data types and pattern matching instead of classes and private methods.

 

Typescript doesn't have algebraic data types? It has type unions and intersections. What else does it need to qualify?

Totally agree pattern matching would be awesome though.