typescript

Introduction to fp-ts

Functional programming is a revolutionary approach to software development.

Instead of relying on changing states and sequences of commands, it treats computation as mathematical functions. This elegant paradigm, with its emphasis on pure, side-effect-free functions, is gaining traction in modern programming languages.

To understand the benefits of fp-ts, we need to understand first functional programming.

Why functional programming?

Because JavaScript is an agnostic language related to programming paradigms, it is possible to write code in a functional style. However, it is not a functional language, and we must keep in mind the rules to avoid writing non-functional code.

Let’s see a few examples comparing imperative and declarative code.

Imperative vs Declarative code

In the imperative approach, you typically use loops and mutable variables to achieve tasks. For example:

For a given array of numbers [1, 2, 3, 4, 5] we want the sum of the even numbers squared.

const numbers = [1, 2, 3, 4, 5]

// Imperative approach
let sum = 0
for (let i = 0; i < numbers.length; i++) {
    if (numbers[i] % 2 === 0) {
        sum += numbers[i] * numbers[i]
    }
}

// Declarative approach
const sum = numbers
    .filter(n => n % 2 === 0)
    .map(n => n * n)
    .reduce((acc, n) => acc + n, 0)

This is why declarative code is getting more and more popular: it is easier to read and understand. Even with a simple example like this, it is clear the 3-step operation we are doing: filter, map and reduce.

The idea behind functional programming, is to create small, reusable functions that define operations on data, which later can be composed to create more complex operations just chaining them. Of course, this is a very basic example, but let’s imagine each of these 3 steps were more complex operations. We could extract them to named functions like even, square and sum and then compose them like this:

const sum = numbers.filter(even).map(square).reduce(sum)

With fp-ts, we have a way to manage this operation chaining with fully inferred typed code, so we can focus on developing the operations needed by the business logic without the fear to include non-functional steps in our system.

What is fp-ts?

fp-ts is a library for typed functional programming in TypeScript. It is inspired by Haskell and other functional languages. It provides a set of utilities to bring the most common type classes, data structures and abstraction patterns to TypeScript.

pipe and flow

The function pipe() defines a chain of operations

import { pipe } from 'fp-ts/function'

// Imagine we have some functions that make certain operations
const addOne = (n: number) => n + 1
const double = (n: number) => n * 2
const toString = (n: number) => `The result is ${n}`
const log = (s: string) => console.log(s)

// And we want to combine them with our input in a certain order to get the final result
const input = 2

// Instead of doing this
log(toString(double(addOne(input))))

// or even worse, this
const intermediateResult = addOne(input)
const intermediateResult2 = double(intermediateResult)
const intermediateResult3 = toString(intermediateResult2)
log(intermediateResult3)

// We can easily use pipe() to do do it in a much more readable way
pipe(input, addOne, double, toString, log) // The result is 6

Each output of the previous function is passed as input to the next one. The first argument is the input of the whole flow, and the result type will be the return type of the last function used.

This is a very common pattern in functional programming, and it’s called a pipeline.

Another one is flow(). It is very similar to pipe(), but instead of using it to execute a series of operations, it is used to define a new function that will execute those operations. Think about it like a reusable pipeline.

import { flow } from 'fp-ts/function'

// Remember the functions from the previous example?
// We can define 2 reusable flows:
const addOneAndDouble = flow(addOne, double)
const doubleAndToString = flow(double, toString)

// So later we can use them in our pipes
pipe(input, addOneAndDouble, doubleAndToString, log)

So, pipe is just a chain of operations, and flow is a reusable chain of operations.
You can think about it like running operations directly in our code, or defining a function that will run those operations.

// runs the operations
pipe(1, addOne, double, toString, log) // void

// defines a flow of operations
const myFlow = flow(addOne, double, toString, log) // (n: number) => void
myFlow(1) // void

Option

The Option type is a way of representing a value that may or may not exist. It is very similar to null or undefined, but it is safer because it forces you to handle the case where the value doesn’t exist.

It prevents you from getting errors like Cannot read property 'x' of undefined or Cannot read property 'x' of null, allowing you to write safer code without ending up with a bunch of if statements everywhere.

So, let’s check how it works.
Imagine we have a function that inverts a value. Because it can’t invert(0), we may throw an error in that specific breaking case.

const invert = (n: number): number => {
    if (n === 0) throw new Error("Can't invert 0")
    return 1 / n
}

Here, basically we are lying in the return type, as it is not a pure function. The return type is : number, but is it true? Does it actually return a number always? No, sometimes it throws an error. So, we are lying.

Here is where it comes Option. You could think about it like a standardization of the null and undefined values, but with a lot of utilities to manage it.

import * as O from 'fp-ts/Option'

// Option<number> means that the value may be a number or may not exist
const invert = (n: number): O.Option<number> => (n === 0 ? O.none : O.some(1 / n))

// To consume this function, we can use patter matching
// Here it is done with `match()` and `fold()` functions
// (both are the same, it is just an alias)
pipe(
    5, // input
    invert,
    match(
        () => "Can't invert 0",
        n => `The result is ${n}`,
    ),
    log, // log the messages
)

// There are other ways to manage it, like `getOrElse()`
// Imagine that in case the input is 0, we want to return another value as default
pipe(
    0, // input
    invertOption,
    getOrElse(() => 5), // default value
    // keep operating with numbers
)

In the first case with the match, we return a string in both branches, so we could continue the pipeline with log without worrying about problems with the invert. In the second case, the getOrElse fills the gap of the invert function with a default value, so we can continue operating with numbers normally without messing the return type of the next steps.

O.map + O.flatten + O.chain

Imagine we have a table with movie titles and ratings, and we want a function that when we call it with the array of titles, it returns the title of the movie with the highest rating formatted as the most rated movie is: .toUpperCase(). <pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#79B8FF"> *</span><span style="color:#F97583"> as</span><span style="color:#E1E4E8"> O </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'fp-ts/lib/Option'</span></span> <span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { flow } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'fp-ts/lib/function'</span></span> <span class="line"></span> <span class="line"><span style="color:#F97583">type</span><span style="color:#B392F0"> Movie</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {</span></span> <span class="line"><span style="color:#FFAB70"> title</span><span style="color:#F97583">:</span><span style="color:#79B8FF"> string</span></span> <span class="line"><span style="color:#FFAB70"> rating</span><span style="color:#F97583">?:</span><span style="color:#79B8FF"> number</span></span> <span class="line"><span style="color:#E1E4E8">}</span></span> <span class="line"></span> <span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> movies</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Array</span><span style="color:#E1E4E8"><</span><span style="color:#B392F0">Movie</span><span style="color:#E1E4E8">> </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> [</span></span> <span class="line"><span style="color:#E1E4E8"> { title: </span><span style="color:#9ECBFF">'Interestellar'</span><span style="color:#E1E4E8">, rating: </span><span style="color:#79B8FF">4.3</span><span style="color:#E1E4E8"> },</span></span> <span class="line"><span style="color:#E1E4E8"> { title: </span><span style="color:#9ECBFF">'The Martian'</span><span style="color:#E1E4E8">, rating: </span><span style="color:#79B8FF">4.5</span><span style="color:#E1E4E8"> },</span></span> <span class="line"><span style="color:#E1E4E8"> { title: </span><span style="color:#9ECBFF">'Apollo 13'</span><span style="color:#E1E4E8">, rating: </span><span style="color:#79B8FF">4.2</span><span style="color:#E1E4E8"> },</span></span> <span class="line"><span style="color:#E1E4E8"> { title: </span><span style="color:#9ECBFF">'Unrated movie'</span><span style="color:#E1E4E8"> },</span></span> <span class="line"><span style="color:#E1E4E8">]</span></span> <span class="line"></span> <span class="line"><span style="color:#6A737D">// For achieving our goal, we gonna define an utility function that will return the most rated movie</span></span> <span class="line"><span style="color:#6A737D">// In case we pass an empty array or an array with only unrated movies, it will return none</span></span> <span class="line"><span style="color:#F97583">const</span><span style="color:#B392F0"> _getMostRated</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">movies</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Array</span><span style="color:#E1E4E8"><</span><span style="color:#B392F0">Movie</span><span style="color:#E1E4E8">>)</span><span style="color:#F97583">:</span><span style="color:#B392F0"> O</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">Option</span><span style="color:#E1E4E8"><</span><span style="color:#B392F0">Movie</span><span style="color:#E1E4E8">> </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span> <span class="line"><span style="color:#F97583"> return</span><span style="color:#E1E4E8"> movies.</span><span style="color:#B392F0">filter</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">m</span><span style="color:#F97583"> =></span><span style="color:#E1E4E8"> m.rating).</span><span style="color:#79B8FF">length</span><span style="color:#6A737D"> // no rated movies -> O.none</span></span> <span class="line"><span style="color:#F97583"> ?</span><span style="color:#79B8FF"> O</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">some</span><span style="color:#E1E4E8">(</span></span> <span class="line"><span style="color:#E1E4E8"> movies</span></span> <span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">filter</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">m</span><span style="color:#F97583"> =></span><span style="color:#E1E4E8"> m.rating) </span><span style="color:#6A737D">// only compare movies with rating</span></span> <span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">reduce</span><span style="color:#E1E4E8">((</span><span style="color:#FFAB70">acc</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">movie</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> (movie.rating </span><span style="color:#F97583">></span><span style="color:#E1E4E8"> acc.rating </span><span style="color:#F97583">?</span><span style="color:#E1E4E8"> movie </span><span style="color:#F97583">:</span><span style="color:#E1E4E8"> acc)),</span></span> <span class="line"><span style="color:#E1E4E8"> )</span></span> <span class="line"><span style="color:#F97583"> :</span><span style="color:#79B8FF"> O</span><span style="color:#E1E4E8">.none</span></span> <span class="line"><span style="color:#E1E4E8">}</span></span> <span class="line"></span> <span class="line"><span style="color:#6A737D">// Now we can define a flow that uses that function along with others</span></span> <span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> getMostRatedMovieTitle</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> flow</span><span style="color:#E1E4E8">(</span></span> <span class="line"><span style="color:#E1E4E8"> _getMostRated, </span><span style="color:#6A737D">// Get the most rated movie</span></span> <span class="line"><span style="color:#79B8FF"> O</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">map</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">movie</span><span style="color:#F97583"> =></span><span style="color:#E1E4E8"> movie.title), </span><span style="color:#6A737D">// Get the title of the movie</span></span> <span class="line"><span style="color:#79B8FF"> O</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">map</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">s</span><span style="color:#F97583"> =></span><span style="color:#E1E4E8"> s.</span><span style="color:#B392F0">toUpperCase</span><span style="color:#E1E4E8">()), </span><span style="color:#6A737D">// Convert the title to uppercase</span></span> <span class="line"><span style="color:#79B8FF"> O</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">map</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">s</span><span style="color:#F97583"> =></span><span style="color:#9ECBFF"> `The most rated movie is ${</span><span style="color:#E1E4E8">s</span><span style="color:#9ECBFF">}`</span><span style="color:#E1E4E8">), </span><span style="color:#6A737D">// Add a prefix to the title</span></span> <span class="line"><span style="color:#79B8FF"> O</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">getOrElse</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">=></span><span style="color:#9ECBFF"> 'There is no rated movie'</span><span style="color:#E1E4E8">), </span><span style="color:#6A737D">// In case we don't have a rated movie</span></span> <span class="line"><span style="color:#E1E4E8">)</span></span> <span class="line"></span> <span class="line"><span style="color:#6A737D">// Now we can use our utility function to get the most rated movie</span></span> <span class="line"><span style="color:#E1E4E8">console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#B392F0">getMostRatedMovieTitle</span><span style="color:#E1E4E8">(movies)) </span><span style="color:#6A737D">// The most rated movie is THE MARTIAN</span></span> <span class="line"></span> <span class="line"><span style="color:#6A737D">// And in case we don't have any rated movie</span></span> <span class="line"><span style="color:#E1E4E8">console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#B392F0">getMostRatedMovieTitle</span><span style="color:#E1E4E8">([])) </span><span style="color:#6A737D">// There is no rated movie</span></span></code>

Sometimes we may have to map an Option<> type, and then we end up with a nested Option<Option<>>.

For example, if we have a function that returns an Option<> and we want to map it, we end up with a nested Option<Option<>>. To solve this, we have a function called flatten() that will flat it.

flow(
    maybeValue, // -> Option<number>
    O.map(inverse), // ->  Option<Option<number>>
    O.flatten, // -> Option<number>
)

Because this pattern of O.map + O.flatten is very common, there is a function called chain() that does both things at the same time (known in some languages as flatMap).

flow(
    maybeValue, // -> Option<number>
    O.chain(inverse), // -> Option<number>
)

This allows to “chain” multiple O.chain() calls without duplicating the lines having to flat the result of each one.

O.fromPredicate

It is used to create an Option<> from a (n) => boolean function.

Imagine we have a isEven(n: number): boolean function. And we want to return some(n) if the number is even, or none if it is not. Instead of wrapping the (n) => boolean into an new one (n) => Option<number>, we can use fromPredicate().

const isEven = (n: number): boolean => n % 2 === 0

pipe(
    2,
    O.fromPredicate(isEven), // -> some(2)
)

O.alt

It is used to create a conditional flow. The equivalent to else in an if.

Imagine that we have an array of kids, and we want to select who is going to be the leader of the group depending on two conditions:

  1. If a unique kid is the oldest one, he will be the leader
  2. If it is a draw, the kid with better grades will be the leader
  3. If it is a draw again, there is no leader
type Kid = {
    name: string
    age: number
    grades: number
}

const kids = [
    { name: 'John', age: 13, grades: 4 },
    { name: 'Jose', age: 12, grades: 3 },
]

// condition 1
const getUniqueOldestKid = (kids: Kid[]): O.Option<Kid> => {
    if (!kids.length) return O.none // If there are no kids, return none
    if (kids.length === 1) return O.some(kids[0]) // If there is only one kid, return it

    kids.sort((a, b) => b.age - a.age) // Sort by age
    return kids[0].age === kids[1].age ? O.none : O.some(kids[0]) // If it is a draw, return none
}

// condition 2
const getUniqueTallestKid = (kids: Kid[]): O.Option<Kid> => {
    if (!kids.length) return O.none // If there are no kids, return none
    if (kids.length === 1) return O.some(kids[0]) // If there is only one kid, return it

    kids.sort((a, b) => b.grades - a.grades) // Sort by grades
    return kids[0].grades === kids[1].grades ? O.none : O.some(kids[0]) // If it is a draw, return none
}

pipe(
    kids,
    getUniqueOldestKid,
    O.alt(() => getUniqueTallestKid(kids)), // If there is no unique oldest kid, get the tallest one
    O.map(kid => `The leader is ${kid.name}`),
    O.getOrElse(() => 'Total DRAW: There is no leader'),
)

By this way, we can set an alternative operation to be done in case the first one returns none, allowing to continue the flow.

Either

The Option<> was used to represent a value that may exists or not. This has a problem, that in case of not, O.none does not provide any information about why it does not exist. What if we want to know the reason to handle it later?

Here comes the Either<> type. It is used to represent a value that can be of two types: Left<> (error) or Right<> (value).

Imagine we have a function to execute the payment in a checkout process. This function may succeed (Right<PaymentID>) or fail (Left<Error>).

type Account = {
    balance: number
    frozen: boolean
}

// This would be a possible implementation, with 2 possible errors
const pay =
    (amount: number) =>
    (account: Account): E.Either<NotEnoughBalance | AccountFrozen, Account> =>
        account.frozen
            ? E.left({ type: 'AccountFrozen', message: 'The account is frozen!' })
            : account.balance < amount
            ? E.left({ type: 'NotEnoughBalance', message: 'Not enough balance!' })
            : E.right({ ...account, balance: account.balance - amount })

pipe(
    { balance: 100, frozen: false },
    pay(50), // -> Right({ balance: 50, frozen: false })
)

pipe(
    { balance: 100, frozen: true },
    pay(50), // -> Left({ type: 'AccountFrozen', message: 'The account is frozen!' })
)

Again, some developers may think that this is the same as throwing 2 different errors in each case. Let’s try to do it without fp-ts for a moment and see the difference between both approaches.

const pay =
    (amount: number) =>
    (account: Account): Account => {
        if (account.frozen) throw new Error('The account is frozen!')
        if (account.balance < amount) throw new Error('Not enough balance!')
        return { ...account, balance: account.balance - amount }
    }

What is now the return type? : Account. Is it true? Does it always return an account as result? No, sometimes it throws an error. So, we are lying again. Knowing not only the possibility that the function may throw, but also with what kind of error, is a huge advantage from the consumer point of view.

This can be combined later with a match() to handle each case.

pipe(
    { balance: 100, frozen: true },
    pay(50), // -> Left({ type: 'AccountFrozen', message: 'The account is frozen!' })
    match(
        left => console.error(left.message), // -> The account is frozen!
        right => console.log(right.balance), // -> 50
    ),
)

E.tryCatch

Imagine we want to create a function that returns the result of JSON.parse() of a string. This function may succeed (Right<JSON>), or fail (Left<Error>).

const jsonParse = (text: string): E.Either<Error, unknown> => {
    try {
        return E.right(JSON.parse(text))
    } catch (e) {
        const error = e instanceof Error ? e : new Error(String(e))
        return E.left(error)
    }
}

This is pretty verbose and not functional at all, so fp-ts provides a function called tryCatch() that helps to manage this.

const jsonParse = (text: string): E.Either<Error, unknown> =>
    E.tryCatch(
        () => JSON.parse(text),
        e => (e instanceof Error ? e : new Error(String(e))),
    )

This allows to not use try/catch block and write the code much more readable.

Also, do you see how the exact input of our function jsonParse(text: string) will be the input of the JSON.parse(text)?

When this happens, we can use the similar block E.tryCatchK().

// Types both for input and output are inferred! 🎉
const jsonParse = E.tryCatchK(
    JSON.parse,
    e => (e instanceof Error ? e : new Error(String(e)))
)