Articles Technical notes · Cahier nº 1NoticeBack cover Back cover

Technical notesCahier nº 1

Crafting our own Option type in typescript

Technical notes · nº 1NoticeCrafting our own Option type in typescript

Notice

Crafting our own Option type in typescript

Written
May 2022
Series
Technical notes, nº 1 of 1
Length
8 minutes · – pages
Topics
functional programming · TypeScript
Status
First edition

In this cahier

  1. 1Who is that article for–
  2. 2What's our goal?–
  3. 3The program–
  4. 4Finding a type saying a value can be empty–
  5. 5Rewrite of our program using the new signature–
  6. 6Giving Option some friends–
  7. 7Conclusion–
  8. 8If you have more time–

1 · Who is that article for

In that article we will build our own Option type from the ground up.

It requires a bit of typescript knowledge. It will be a step by step process.

2 · What's our goal?

We will start from a simple program that

  • Asks the user its name
  • Crafts a message depending on whether the user provided a name or not
  • Logs a greeting message

What we will be looking for is a way to express that program using types first.

3 · The program

function userInput(): string | null {
    // some implementation
}

/** we will store the greeting message in that variable */
let greetingMessage: string

console.log('What is your name?')

const userName: String | null = userInput() // ask for the user name

if (userName !== null) {
    greetingMessage = `Hello user, I will call you ${userName} from now on`
} else {
    greetingMessage = 'Hello stranger! It seems I didn\'t quite catch your name'
}

console.log(greetingMessage)

code scrolls down and sideways · turn the phone for the full width

4 · Finding a type saying a value can be empty

As of now the userInput function can either return a string or null but what if we wanted to return only one stable type instead of two?

From here, we need to find this new type. It should convey the idea that userInput can either return some value or none at all.

Starting with this in mind we can define two new types: Some and None

// None tells that it doesn't hold any value
type None = { isSome: false }

// Some tells it has some value of type A and hold it in a `value` field.
// A could be any type we could think of as in Some<String> or Some<number>.
type Some<A> = { isSome: true, value: A }

code scrolls down and sideways · turn the phone for the full width

So our userInput can now return Some<String> | None, for our convenience we will introduce a type saying exactly this:

// An Option can either have Some value of type A or None at all
type Option<A> = None | Some<A>

code scrolls down and sideways · turn the phone for the full width

which leads us to have a new signature for our userInput function conveying within one type that it is allowed to return nothing.

function userInput(): Option<String> {
    // some implementation
}

code scrolls down and sideways · turn the phone for the full width

5 · Rewrite of our program using the new signature

Leveraging the new signature we can make the if statement closer to english

let greetingMessage: string
console.log('What is your name?')
const userName: Option<String> = userInput()

if (userName.isSome) {
    greetingMessage = `Hello user, I will call you ${userName.value} from now on`
} else {
    greetingMessage = 'Hello stranger! It seems I didn\'t quite catch your name'
}

console.log(greetingMessage)

code scrolls down and sideways · turn the phone for the full width

userName.value can only be accessed if we first check userName.isSome making the program already safer.

6 · Giving Option some friends

Now that we have a cool type, we will give that type some functions to help it make it nicer.

The map function

We are looking into isSome then accessing the value, we could provide a function that says something like:

Given an option which might contain A
 
apply this function going from A to B

So we have an option which might contain B

code scrolls down and sideways · turn the phone for the full width

We will call that function a map since it maps our Option<A> to Option<B> using our function.

Map signature will look like that:

function map<A, B>(transformation: (value: A) => B): (option: Option<A>) => Option<B> {
}

code scrolls down and sideways · turn the phone for the full width

so we can now rewrite our program this way:

function greetAKnownUser(userName: String) {
    return `Hello user, I will call you ${userName} from now on`
}

let greetingMessage: string

console.log('What is your name?')

const userName: Option<String> = userInput()
const knownUserMessage = map(greetAKnownUser)(userName)

if (knownUserMessage.isSome) {
    greetingMessage = knownUserMessage.value
} else {
    greetingMessage = 'Hello stranger! It seems I didn\'t quite catch your name'
}

console.log(greetingMessage)

code scrolls down and sideways · turn the phone for the full width

It starts to look closer and closer to some english statements, the one bit staying is this if /else.

The getOrElse function

What could we imagine from here to totally get rid of that if else?

We are now looking for a function saying something like:

Given an option which might contain A

And a default value of type A

Pick the value contained in the option, on None, pick the default value

Return a value of type A in any case

code scrolls down and sideways · turn the phone for the full width

In other words, we would like to get the value or else get the default so the name seem clear, it is the getOrElse function:

/**
 * If option has some value, then it will return it, otherwise it will return the default passed value
 */
function getOrElse<A>(orElseValue: A): (option: Option<A>) => A {
}

code scrolls down and sideways · turn the phone for the full width

Let's rewrite our program using that

function greetAKnownUser(userName: String) {
    return `Hello user, I will call you ${userName} from now on`
}

const greetStranger =
    'Hello stranger! It seems I didn\'t quite catch your name'

console.log('What is your name?')

const userName: Option<String> = userInput()
const knownUserMessage = map(greetAKnownUser)(userName)
const greetingMessage = getOrElse(greetStranger)(knownUserMessage)

console.log(greetingMessage)

code scrolls down and sideways · turn the phone for the full width

Stopping here for a little, there are multiple things to say about what we have.

First, there is no longer a let awaiting the program to go in one of our if/else statement to be filled. From now on there is only constant using the value of the previous constant to decide its own value.

Second, there is no more if/else and in broader manner, there is no more imperative statements. We didn't provide any implementation yet for map and getOrElse and looking at the code we don't need it to know what the program does!

It is still not that nice to read, we could have a final utils to help us on the reading part

The pipe function

The pipe function is a dumb function. It takes a list of functions and calls the next one with the result of the previous one.

function pipe<A>(...ListOfFunctions): A {
    // some implementation
}

code scrolls down and sideways · turn the phone for the full width

Which gives us this final result:

const greetAKnownUser = (userName: String) =>
    `Hello user, I will call you ${userName} from now on`

const greetStranger =
    'Hello stranger! It seems I didn\'t quite catch your name'

console.log('What is your name?')

const greetingMessage = pipe(
    userInput(),
    map(greetAKnownUser),
    getOrElse(greetStranger)
)

console.log(greetingMessage)

code scrolls down and sideways · turn the phone for the full width

7 · Conclusion

We now have the same program written twice:

  • the first one doesn't express the result of the program but tells what steps the computer should exactly take to obtain the correct message.
  • The second version declares the result of the program without telling which steps to take to achieve that result.

The declarative version tells what result we expect without telling how it should achieve such result

What did we gain introducing the Option type in our code? The first thing we got is safety! Indeed, by design our code won't allow to end up in a null situation anymore therefore anyone who works using our Option type will have to treat null edge cases.

We also gained in reusability and composability. We can split that program in multiple sub-functions that could be easily reused since it operates against the same type encasing the same assumptions.

On the other hand, we lost a bit in readability, after a while you get use to write and read your code in a functional flavor. It requires some habits and training making the code less accessible for the majority of the developers' community.

We also lost contact with the actual implementation, which imply that we trust whoever implemented getOrElse, map or pipe to have the correct implementation and the most stack safe possible.

So shall we invest more time in it? Knowing how to write such code seem to me important to understand that there is many ways to write the same code with different constraint and goals. In context of safety (our code can't produce uncontrolled edge-cases), bringing such functional practices fits well.

8 · If you have more time

About testing

We can now write things like:

function getUserGreetingFromOption(option: Option<String>): String {
    const knownUserMessage = map(greetAKnownUser)(userName)
    const greetingMessage = getOrElse(greetStranger)(knownUserMessage)
    return greetingMessage
}

const greetingMessage = getUserGreetingFromOption(userInput())

console.log(greetingMessage)

code scrolls down and sideways · turn the phone for the full width

Why is that ability nice? because it will ease testing our system behavior by providing inputs and outputs. We no longer requires to mock userInput if we wish to test the return message we can directly do something like:

describe('getUserGreetingFromOption', () => {
    it('should greet stranger on none', () =>
        expect(getUserGreetingFromOption({isSome: false})).toEqual(greetStranger))

    it('should greet Amy', () =>
        expect(getUserGreetingFromOption({isSome: true, value: 'Amy'}))
            .toEqual(greetAKnownUser('Amy')))
})

code scrolls down and sideways · turn the phone for the full width

Full implementation

////// Option
/// Definitions
// None tells that it doesn't hold any value
type None = { isSome: false }

// Some tells it has some value of type A and hold it in a `value` field
type Some<A> = { isSome: true, value: A }

// An Option can either have Some value of type A or None at all
type Option<A> = None | Some<A>

/// constructors
const some: <A>(value: A) => Option<A> = value => ({
    isSome: true,
    value
})

const none: () => None = () => ({
    isSome: false
})

/// destructors
const getOrElse: <A>(orElseValue: A) => (option: Option<A>) => A =
    defaultValue => option => option.isSome ? option.value : defaultValue

/// operations
const map: <A, B>(transformation: (value: A) => B) => (option: Option<A>) => Option<B> =
    transformation => option => option.isSome ? some(transformation(option.value)) : none()


////// Functions
/// Pipes
const pipe3: <A, B, C>(fn1: A, fn2: (a: A) => B, fn3: (b: B) => C) => C =
    (fn1, fn2, fn3) => fn3(fn2(fn1))


// ******************************************
const userInput = (): Option<String> =>
    some('Amy') // you can try with none()

const greetAKnownUser = (userName: String) =>
    `Hello! I will call you ${userName} from now on`

const greetStranger =
    'Hello! It seems I didn\'t quite catch your name'

console.log('What is your name?')

const greetingMessage = pipe3(
    userInput(),
    map(greetAKnownUser),
    getOrElse(greetStranger)
)

console.log(greetingMessage)

code scrolls down and sideways · turn the phone for the full width

This article has been reviewed by Erwan Guyomarc'h and Alice Rouquette.

Where to next

This series
Nº 2 is in preparation.
This cahier
jarleton.dev/notes/1/

About the author

Paul Jarleton

Agentic-ready Platform and DevEx

With more than 15 years of experience from scales-up to Spotify,
I sharpened skills within the Cloud, Platform and DevEx realm.
I am now happy to bring this knowledge to grow your business foundations.

Your product is only as good as the tools your developers uses everyday.

bio scrolls down

Hire me

Available from March 2027

jarleton.dev · Technical notes nº 1 · 2022

Cahier nº 1

· drag to pan · double-tap to zoom