hazybits

home19 October 2021

How to use `Option` in fp-ts (with code examples)

TypeScriptFunctional Programmingfp-ts

TypeScript and Functional Programming are trending. Fortunately, they both play really well together. In this post, we go through a practical example how you can start using `Option` from fp-ts library in your TypeScript codebase.

Functional Programming in TypeScript

Nowadays, people are generally convinced TypeScript is at least decent. I personally think TypeScript is just great. Despite all the JavaScript's history baggage and having JavaScript as a compilation target, it fits the whole ecosystem perfectly. It doesn't mean it's perfect but a few things are, especially in software. But it's great.

With TypeScript, typed functional programming opportunities are unlocked. If you are still looking for reasons why you should get interested in the topic, it's not a post for you. But if you're familiar with FP from other languages or if you're ready to be thrown in the deep water - here we go.

FP in TypeScript with... fp-ts!

You can write functional code in TypeScript without any external libraries. I did it for years.

Until I discovered fp-ts library. It's a terrific library written and actively maintained by a genius Giulio Canti. It has one major drawback - there's some documentation and learning resources but not that many. But this post should make a situation a tiny bit better... ;)

Handling a simple optional value

Optional values are everywhere. And there are many approaches how there are solved.

Let's say you need to read some configuration.

type AppConfig = {
apiUrl: string;
signingSecret: string;
};
function readAppConfig(): AppConfig {
const apiUrl = process.env.API_URL;
const signingSecret = process.env.SIGNING_SECRET;
return { apiUrl, signingSecret };
}

Well, it won't compile (if you have TypeScript strictNullChecks on, which you totally should have on) because both API_URL and SIGNING_SECRET can be unset and therefore undefined. And we all know it really happens from time to time.

So we can alter our AppConfig to account for this scenario like so:

type AppConfig = {
apiUrl: string | undefined;
signingSecret: string | undefined;
};

It solves the compilation issue but it doesn't guarantee that application config is valid (assuming we need to have both properties set to do anything useful). This change made a whole thing compile but didn't solve the underlying issue. Rather than sweeping problems under the carpet, let's try to solve them. First step - revert back to initial AppConfig type:

type AppConfig = {
apiUrl: string;
signingSecret: string;
};

and think how we can fix readAppConfig definition.

We might do something like this:

function readAppConfig(): AppConfig | undefined {
const apiUrl = process.env.API_URL;
const signingSecret = process.env.SIGNING_SECRET;
return typeof apiUrl === "string" && signingSecret === "string"
? { apiUrl, signingSecret }
: undefined;
}

Now it works. But we wanted to use an Option from fp-ts! We can do it like this:

import { fromNullable, Option } from "fp-ts/Option";
function readAppConfig(): Option<AppConfig> {
const apiUrlOpt: Option<string> = fromNullable(process.env.API_URL);
const signingSecretOpt: Option<string> = fromNullable(process.env.SIGNING_SECRET);
return ???
}

Now both values are wrapped in Option thing/context/container/monad (pick whichever best explains what just happened). But what should we return? Well, we need to express "look into both options and if they both hold a value, return it, otherwise return none". Alternatively, you may think of that as a logical AND operator.

function readAppConfig(): Option<AppConfig> {
const apiUrlOpt: Option<string> = fromNullable(process.env.API_URL);
const signingSecretOpt: Option<string> = fromNullable(
process.env.SIGNING_SECRET
);
const sequenced: Option<[string, string]> = sequenceT(Apply)(
apiUrlOpt,
signingSecretOpt
);
return pipe(
sequenced,
map(([apiUrl, signingSecret]) => ({ apiUrl, signingSecret }))
);
}

Now it compiles (and works)! But it's not very concise. Let's see how we can make it more succinct. Thanks to functional programming (if we preserve referential transparency) we should be able to extract and inline variables without worrying about altering our program's behaviour.

import { fromNullable, Option, map, Apply } from "fp-ts/Option";
import { sequenceT } from "fp-ts/lib/Apply";
import { pipe } from "fp-ts/lib/function";
function readAppConfig(): Option<AppConfig> {
return pipe(
sequenceT(Apply)(
fromNullable(process.env.API_URL),
fromNullable(process.env.SIGNING_SECRET)
),
map(([apiUrl, signingSecret]) => ({ apiUrl, signingSecret }))
);
}

Here you are. Now readAppConfig reads available configuration and returns optional value. In case both API_URL and SIGNING_SECRET hold a string, it's Some with AppConfig value, otherwise None.

Was it worth it?

In this short example, it's not obvious. Modern JavaScript/TypeScript is quite effective at handling nullable values. Operators like ?. make it even better.

Our fp-tsed attempt is without doubt more complex and not really more readable... Hmm...

My rule of thumb for fp-ts-ing

For simple cases, I like the simplicity of raw JS/TS. For code close to React components - I tend not to use fp-ts. I tried a few times and usually backtracked. There was a lot of into-the-Option and out-of-the-Option ceremony that was simply not worth it (in my case).

But for anything more complex (closer to app logic than just UI) - this is where fp-ts shines and is my preferred approach. Don't forget that it's just a short snippet from a whole codebase. Functional Programming is a lot about composition. And to experience composition, we need more pieces than just one function.

What's next?

We've just went through Options 101. Options are much more powerful constructs and also a part of a bigger story. But that's... that's a topic for the next article.

Thank you for your time.

I hope you enjoyed the post 😊.

If you'd like to discuss something in more detail or simply let me know what do you think, connect with me on Twitter or reach out via email.


Until the next one,
@catchergeese




Read more