Skip to content

Functional Programming in TypeScript Using the fp-ts Library: Deep Dive Into Option's Methods and Other Useful fp-ts Operators

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Welcome back to our blog series on functional programming with fp-ts! In our previous posts, we talked about the building block of fp-ts library: Pipe and Flow operators and we introduced one of the most useful types in the library: Option type. Let's start to use our knowledge and combine all the blocks: in this blog post, we'll take a deep dive into fp-ts' Option type, and explore its fundamental methods such as fold, fromNullable, and getOrElse. We'll then leverage the map, flatten, and chain operators, combining them with our powerful (and already known) operator to compose expressive and concise code.

Understanding Option

The Option type, also known as Maybe, represents values that might be absent. It is particularly useful for handling scenarios where a value could be missing, eliminating the need for explicit null checks. fp-ts equips us with a rich set of methods and operators to work with Option efficiently.

fold: The fold method allows us to transform an Option value into a different type by providing two functions: one for the None case, and another for the Some case. The pipe operator enhances the readability of the code by enabling a fluent and concise syntax.

import { Option, none, some } from 'fp-ts/lib/Option';
import { fold } from 'fp-ts/lib/Option';
import { pipe } from 'fp-ts/lib/pipeable';

const value: Option<number> = some(10);

const result = pipe(
  value,
  fold(
    () => 'No value',
    (x: number) => `Value is ${x}`
  )
); // result: "Value is 10"

In this example, we have an Option value some(10), representing the presence of the number 10. We use the pipe operator from fp-ts to chain the value through the fold function, passing in two functions. The first function, () => 'No value', handles the None case when the Option is empty. The second function, (x: number) => Value is ${x}, handles the Some case and receives the value inside the Option (in this case, 10). The resulting value is "Value is 10".

fromNullable: The fromNullable function converts nullable values (e.g., null or undefined) into an Option. We can leverage pipe to make the code more readable and maintainable.

import { Option, fromNullable } from 'fp-ts/lib/Option';
import { pipe } from 'fp-ts/lib/pipeable';

const value: string | null = 'Hello, world!';

const optionValue: Option<string> = pipe(value, fromNullable);

In the example, we have a string value 'Hello, world!', which is not nullable. However, by using the pipe operator and passing the value through fromNullable, fp-ts internally checks if the value is null or undefined. If it is, it produces a None value, indicating the absence of a value. Otherwise, it wraps the value inside Some. So, in this case, the resulting optionValue is Some("Hello, world!").

getOrElse: The getOrElse method allows us to extract the value from an Option or provide a default value if the Option is None. Pipe operator aids in composing the getOrElse function with other operations seamlessly.

import { Option, some, none } from 'fp-ts/lib/Option';
import { getOrElse } from 'fp-ts/lib/Option';
import { pipe } from 'fp-ts/lib/pipeable';

const optionValue: Option<number> = some(10);

const value = pipe(optionValue, getOrElse(() => 0)); // value: 10

const noneValue: Option<number> = none;

const defaultValue = pipe(noneValue, getOrElse(() => 0)); // defaultValue: 0

In the first example, we have an Option value some(10). Using the pipe operator, and passing the Option through getOrElse, we provide a function () => 0 as a default value. Since the Option is Some(10), the function is not executed, and the resulting value is 10. In the second example, we have an Option value none, representing the absence of a value. Again, using the pipe operator and getOrElse, we provide a default value of 0. Since the Option is None, the function () => 0 is executed, resulting in the default value of 0.

Map, Flatten, and Chain Operators

Building upon the foundational methods of Option, fp-ts provides powerful operators like map, flatten, and chain, which enable developers to compose complex operations in a functional and expressive manner.

map: The map operator allows us to transform the value inside an Option using a provided function. It applies the function only if the Option is Some.

import { Option, some } from 'fp-ts/lib/Option';
import { map } from 'fp-ts/lib/Option';
import { pipe } from 'fp-ts/lib/pipeable';

const optionValue: Option<number> = some(10);

const mappedValue: Option<string> = pipe(optionValue, map((x: number) => `Value is ${x}`)); // mappedValue: Some("Value is 10")

In this example, we have an Option value some(10). Using the pipe operator and passing the Option through map, we provide a function (x: number) => Value is ${x}. Since the Option is Some(10), the function is applied to the value inside the Option, resulting in a new Option Some("Value is 10").

flatten: The flatten operator allows us to flatten nested Options into a single Option. It simplifies the resulting structure when we have computations that may produce an Option inside another Option. The pipe operator assists in composing flatten operations seamlessly.

import { Option, some, none } from 'fp-ts/lib/Option';
import { flatten } from 'fp-ts/lib/Option';
import { pipe } from 'fp-ts/lib/pipeable';

const nestedOption: Option<Option<number>> = some(some(10));

const flattenedOption: Option<number> = pipe(nestedOption, flatten); // flattenedOption: Some(10)

In the example, we have a nested Option some(some(10)). Using the pipe operator and passing the nested Option through flatten, fp-ts flattens the structure, resulting in a single Option Some(10).

chain: The chain operator, also known as flatMap or >>=, combines the functionalities of map and flatten. It allows us to apply a function that produces an Option to the value inside an Option, resulting in a flattened Option.

import { Option, some, none } from 'fp-ts/lib/Option';
import { chain } from 'fp-ts/lib/Option';
import { pipe } from 'fp-ts/lib/pipeable';

const optionValue: Option<number> = some(42);

const chainedValue: Option<string> = pipe(
  optionValue,
  chain((x: number) => (x > 10 ? some(`Value is ${x}`) : none))
);
// chainedValue: Some("Value is 42")

const noneValue: Option<number> = none;

const noneChainedValue: Option<string> = pipe(
  noneValue,
  chain((x: number) => (x > 10 ? some(`Value is ${x}`) : none))
);
// noneChainedValue: None

In the first example, we have an Option value some(42). Using the pipe operator and passing the Option through chain, we provide a function that checks if the value is greater than 10. If it is, it returns Some(Value is ${x}), where x is the value inside the Option. Since the value is 42, which is greater than 10, the resulting Option is Some("Value is 42"). In the second example, we have an Option value none, representing the absence of a value. When passing it through chain with the same function as before, the function is not executed because the Option is None, resulting in None.

Conclusion

fp-ts provides powerful methods and operators for working with the Option type, allowing developers to embrace functional programming principles effectively. By understanding the fold, fromNullable, and getOrElse methods, as well as the map, flatten, and chain operators, and combining them with the pipe operator, developers can write expressive, maintainable, and resilient code. Explore these tools, unlock their potential, and take your functional programming skills to the next level!

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

Understanding Vue's Reactive Data cover image

Understanding Vue's Reactive Data

Introduction Web development has always been about creating dynamic experiences. One of the biggest challenges developers face is managing how data changes over time and reflecting these changes in the UI promptly and accurately. This is where Vue.js, one of the most popular JavaScript frameworks, excels with its powerful reactive data system. In this article, we dig into the heart of Vue's reactivity system. We unravel how it perfectly syncs your application UI with the underlying data state, allowing for a seamless user experience. Whether new to Vue or looking to deepen your understanding, this guide will provide a clear and concise overview of Vue's reactivity, empowering you to build more efficient and responsive Vue 3 applications. So, let’s kick off and embark on this journey to decode Vue's reactive data system. What is Vue's Reactive Data? What does it mean for data to be ”'reactive”? In essence, when data is reactive, it means that every time the data changes, all parts of the UI that rely on this data automatically update to reflect these changes. This ensures that the user is always looking at the most current state of the application. At its core, Vue's Reactive Data is like a superpower for your application data. Think of it like a mirror - whatever changes you make in your data, the user interface (UI) reflects these changes instantly, like a mirror reflecting your image. This automatic update feature is what we refer to as “reactivity”. To visualize this concept, let's use an example of a simple Vue application displaying a message on the screen: ` In this application, 'message' is a piece of data that says 'Hello Vue!'. Let's say you change this message to 'Goodbye Vue!' later in your code, like when a button is clicked. ` With Vue's reactivity, when you change your data, the UI automatically updates to 'Goodbye Vue!' instead of 'Hello Vue!'. You don't have to write extra code to make this update happen - Vue's Reactive Data system takes care of it. How does it work? Let's keep the mirror example going. Vue's Reactive Data is the mirror that reflects your data changes in the UI. But how does this mirror know when and what to reflect? That's where Vue's underlying mechanism comes into play. Vue has a behind-the-scenes mechanism that helps it stay alerted to any changes in your data. When you create a reactive data object, Vue doesn't just leave it as it is. Instead, it sends this data object through a transformation process and wraps it up in a Proxy. Proxy objects are powerful and can detect when a property is changed, updated, or deleted. Let's use our previous example: ` Consider our “message” data as a book in a library. Vue places this book (our data) within a special book cover (the Proxy). This book cover is unique - it's embedded with a tracking device that notifies Vue every time someone reads the book (accesses the data) or annotates a page (changes the data). In our example, the reactive function creates a Proxy object that wraps around our state object. When you change the 'message': ` The Proxy notices this (like a built-in alarm going off) and alerts Vue that something has changed. Vue then updates the UI to reflect this change. Let’s look deeper into what Vue is doing for us and how it transforms our object into a Proxy object. You don't have to worry about creating or managing the Proxy; Vue handles everything. ` In the example above, we encapsulate our object, in this case, “state”, converting it into a Proxy object. Note that within the second argument of the Proxy, we have two methods: a getter and a setter. The getter method is straightforward: it merely returns the value, which in this instance is “state.message” equating to 'Hello Vue!' Meanwhile, the setter method comes into play when a new value is assigned, as in the case of “state.message = ‘Hey young padawan!’”. Here, “value” becomes our new 'Hey young padawan!', prompting the property to update. This action, in turn, triggers the reactivity system, which subsequently updates the DOM. Venturing Further into the Depths If you have been paying attention to our examples above, you might have noticed that inside the Proxy method, we call the functions track and trigger to run our reactivity. Let’s try to understand a bit more about them. You see, Vue 3 reactivity data is more about Proxy objects. Let’s create a new example: ` In this example, when you click on the button, the message's value changes. This change triggers the effect function to run, as it's actively listening for any changes in its dependencies. How does the effect property know when to be called? Vue 3 has three main functions to run our reactivity: effect, track, and trigger. The effect function is like our supervisor. It steps in and takes action when our data changes – similar to our effect method, we will dive in more later. Next, we have the track function. It notes down all the important data we need to keep an eye on. In our case, this data would be state.message. Lastly, we've got the trigger function. This one is like our alarm bell. It alerts the effect function whenever our important data (the stuff track is keeping an eye on) changes. In this way, trigger, track, and effect work together to keep our Vue application reacting smoothly to changes in data. Let’s go back to them: ` Tracking (Dependency Collection) Tracking is the process of registering dependencies between reactive objects and the effects that depend on them. When a reactive property is read, it's "tracked" as a dependency of the current running effect. When we execute track(), we essentially store our effects in a Set object. But what exactly is an "effect"? If we revisit our previous example, we see that the effect method must be run whenever any property changes. This action — running the effect method in response to property changes — is what we refer to as an "Effect"! (computed property, watcher, etc.) > Note: We'll outline a basic, high-level overview of what might happen under the hood. Please note that the actual implementation is more complex and optimized, but this should give you an idea of how it works. Let’s see how it works! In our example, we have the following reactive object: ` We need a way to reference the reactive object with its effects. For that, we use a WeakMap. Which type is going to look something like this: ` We are using a WeakMap to set our object state as the target (or key). In the Vue code, they call this object targetMap. Within this targetMap object, our value is an object named depMap of Map type. Here, the keys represent our properties (in our case, that would be message and showSword), and the values correspond to their effects – remember, they are stored in a Set that in Vue 3 we refer to as dep. Huh… It might seem a bit complex, right? Let's make it more straightforward with a visual example: With the above explained, let’s see what this Track method kind of looks like and how it uses this targetMap. This method essentially is doing something like this: ` At this point, you have to be wondering, how does Vue 3 know what activeEffect should run? Vue 3 keeps track of the currently running effect by using a global variable. When an effect is executed, Vue temporarily stores a reference to it in this global variable, allowing the track function to access the currently running effect and associate it with the accessed reactive property. This global variable is called inside Vue as activeEffect. Vue 3 knows which effect is assigned to this global variable by wrapping the effects functions in a method that invokes the effect whenever a dependency changes. And yes, you guessed, that method is our effect method. ` This method behind the scenes is doing something similar to this: ` The handling of activeEffect within Vue's reactivity system is a dance of careful timing, scoping, and context preservation. Let’s go step by step on how this is working all together. When we run our Effect method for the first time, we call the get trap of the Proxy. ` When running the get trap, we have our activeEffect so we can store it as a dependency. ` This coordination ensures that when a reactive property is accessed within an effect, the track function knows which effect is responsible for that access. Trigger Method Our last method makes this Reactive system to be complete. The trigger method looks up the dependencies for the given target and key and re-runs all dependent effects. ` Conclusion Diving into Vue 3's reactivity system has been like unlocking a hidden superpower in my web development toolkit, and honestly, I've had a blast learning about it. From the rudimentary elements of reactive data and instantaneous UI updates to the intricate details involving Proxies, track and trigger functions, and effects, Vue 3's reactivity is an impressively robust framework for building dynamic and responsive applications. In our journey through Vue 3's reactivity, we've uncovered how this framework ensures real-time and precise updates to the UI. We've delved into the use of Proxies to intercept and monitor variable changes and dissected the roles of track and trigger functions, along with the 'effect' method, in facilitating seamless UI updates. Along the way, we've also discovered how Vue ingeniously manages data dependencies through sophisticated data structures like WeakMaps and Sets, offering us a glimpse into its efficient approach to change detection and UI rendering. Whether you're just starting with Vue 3 or an experienced developer looking to level up, understanding this reactivity system is a game-changer. It doesn't just streamline the development process; it enables you to create more interactive, scalable, and maintainable applications. I love Vue 3, and mastering its reactivity system has been enlightening and fun. Thanks for reading, and as always, happy coding!...

Implementing Dynamic Types in Docusign Extension Apps cover image

Implementing Dynamic Types in Docusign Extension Apps

Implementing Dynamic Types in Docusign Extension Apps In our previous blog post about Docusign Extension Apps, Advanced Authentication and Onboarding Workflows with Docusign Extension Apps, we touched on how you can extend the OAuth 2 flow to build a more powerful onboarding flow for your Extension Apps. In this blog post, we will continue explaining more advanced patterns in developing Extension Apps. For that reason, we assume at least basic familiarity with how Extension Apps work and ideally some experience developing them. To give a brief recap, Docusign Extension Apps are a powerful way to embed custom logic into Docusign agreement workflows. These apps are lightweight services, typically cloud-hosted, that integrate at specific workflow extension points to perform custom actions, such as data validation, participant input collection, or interaction with third-party services. Each Extension App is configured using a manifest file. This manifest defines metadata such as the app's author, support links, and the list of extension points it uses (these are the locations in the workflow where your app's logic will be executed). The extension points that are relevant for us in the context of this blog post are GetTypeNames and GetTypeDefinitions. These are used by Docusign to retrieve the types supported by the Extension App and their definitions, and to show them in the Maestro UI. In most apps, these types are static and rarely change. However, they don't have to be. They can also be dynamic and change based on certain configurations in the target system that the Extension App is integrating with, or based on the user role assigned to the Maestro administrator on the target system. Static vs. Dynamic Types To explain the difference between static and dynamic types, we'll use the example from our previous blog post, where we integrated with an imaginary task management system called TaskVibe. In the example, our Extension App enabled agreement workflows to communicate with TaskVibe, allowing tasks to be read, created, and updated. Our first approach to implementing the GetTypeNames and GetTypeDefinitions endpoints for the TaskVibe Extension App might look like the following. The GetTypeNames endpoint returns a single record named task: ` Given the type name task, the GetTypeDefinitions endpoint would return the following definition for that type: ` As noted in the Docusign documentation, this endpoint must return a Concerto schema representing the type. For clarity, we've omitted most of the Concerto-specific properties. The above declaration states that we have a task type, and this type has properties that correspond to task fields in TaskVibe, such as record ID, title, description, assignee, and so on. The type definition and its properties, as described above, are static and they never change. A TaskVibe task will always have the same properties, and these are essentially set in stone. Now, imagine a scenario where TaskVibe supports custom properties that are also project-dependent. One project in TaskVibe might follow a typical agile workflow with sprints, and the project manager might want a "Sprint" field in every task within that project. Another project might use a Kanban workflow, where the project manager wants a status field with values like "Backlog," "ToDo," and so on. With static types, we would need to return every possible field from any project as part of the GetTypeDefinitions response, and this introduces new challenges. For example, we might be dealing with hundreds of custom field types, and showing them in the Maestro UI might be too overwhelming for the Maestro administrator. Or we might be returning fields that are simply not usable by the Maestro administrator because they relate to projects the administrator doesn't have access to in TaskVibe. With dynamic types, however, we can support this level of customization. Implementing Dynamic Types When Docusign sends a request to the GetTypeNames endpoint and the types are dynamic, the Extension App has a bit more work than before. As we've mentioned earlier, we can no longer return a generic task type. Instead, we need to look into each of the TaskVibe projects the user has access to, and return the tasks as they are represented under each project, with all the custom fields. (Determining access can usually be done by making a query to a user information endpoint on the target system using the same OAuth 2 token used for other calls.) Once we find the task definitions on TaskVibe, we then need to return them in the response of GetTypeNames, where each type corresponds to a task for the given project. This is a big difference from static types, where we would only return a single, generic task. For example: ` The key point here is that we are now returning one type per task in a TaskVibe project. You can think of this as having a separate class for each type of task, in object-oriented lingo. The type name can be any string you choose, but it needs to be unique in the list, and it needs to contain the minimum information necessary to be able to distinguish it from other task definitions in the list. In our case, we've decided to form the ID by concatenating the string "task_" with the ID of the project on TaskVibe. The implementation of the GetTypeDefinitions endpoint needs to: 1. Extract the project ID from the requested type name. 1. Using the project ID, retrieve the task definition from TaskVibe for that project. This definition specifies which fields are present on the project's tasks, including all custom fields. 1. Once the fields are retrieved, map them to the properties of the Concerto schema. The resulting JSON could look like this (again, many of the Concerto properties have been omitted for clarity): ` Now, type definitions are fully dynamic and project-dependent. Caching of Type Definitions on Docusign Docusign maintains a cache of type definitions after an initial connection. This means that changes made to your integration (particularly when using dynamic types) might not be immediately visible in the Maestro UI. To ensure users see the latest data, it's useful to inform them that they may need to refresh their Docusign connection in the App Center UI if new fields are added to their integrated system (like TaskVibe). As an example, a newly added custom field on a TaskVibe project wouldn't be reflected until this refresh occurs. Conclusion In this blog post, we've explored how to leverage dynamic types within Docusign Extension Apps to create more flexible integrations with external systems. While static types offer simplicity, they can be constraining when working with external systems that offer a high level of customization. We hope that this blog post provides you with some ideas on how you can tackle similar problems in your Extension Apps....

"How do I undo my most recent commit?" - Mastering the git reset command cover image

"How do I undo my most recent commit?" - Mastering the git reset command

"How do I undo my most recent commit?" - Mastering the git reset command Git is a powerful version control system, but even experienced developers sometimes make mistakes. Whether you've committed changes to the wrong branch, made a typo in your commit message, or simply want to undo recent changes, the git reset command is your go-to solution. In this post, we'll explore how to use git reset to manage your commit history effectively. The Basics of git reset At its core, git reset moves the HEAD and current branch pointer to a specified commit, effectively "resetting" your working directory to a previous state. The basic syntax is: ` Where specifies how to handle changes in the working directory and staging area, and is the commit you want to reset to. Soft vs. Hard Reset: Understanding the Difference Git reset has two main modes: --soft and --hard. Let's explore each: Soft Reset (--soft) A soft reset moves your HEAD to the specified commit but keeps your changes staged. This is useful when you want to undo commits but keep the changes for recommitting. Example: ` This command undoes the last commit, keeping the changes staged. You can now make additional changes or recommit with a new message. Hard Reset (--hard) A hard reset is more drastic. It moves your HEAD to the specified commit and discards all changes after that commit. Example: ` This command undoes the last commit and discards all changes. Be cautious with this command, as it can lead to data loss! Specifying a Commit Hash Instead of using relative references like HEAD~1, you can specify an exact commit hash: ` This resets to the commit with hash 1a2b3c4, keeping changes staged. Using HEAD~n Notation The HEAD~n notation allows you to specify how many commits back you want to reset. For example: ` This resets to three commits before the current HEAD, discarding all changes. Reverting Pushed Changes If you've already pushed commits you want to undo, you should use git revert instead of git reset to avoid rewriting public history. However, if you must use git reset on a shared branch, you'll need to force push: ` Be extremely cautious with force pushing, as it can cause issues for other developers. Generic Usage of git reset git reset can also be used to unstage changes: ` This removes file.txt from the staging area without changing its contents. Conclusion The git reset command is a powerful tool for managing your Git history. Whether you're undoing recent commits, unstaging changes, or completely resetting your working directory, understanding the different modes of git reset can help you navigate tricky situations in your Git workflow. Remember to use git reset with caution, especially when working with shared repositories. When in doubt, create a backup branch before performing any reset operations....

This Dot AI Field Notes - Anatomy of a Coding Harness cover image

This Dot AI Field Notes - Anatomy of a Coding Harness

A coding agent is not magic, it’s a loop. We call this a harness. The harness is a deterministic layer of code that wraps an LLM. Claude Code is a harness. Codex is a harness. Pi is a harness. The harness, on initialization, provides to the LLM a system prompt defining all tools the harness implements for the LLM. Without the harness, you cannot read or modify files on the user’s local filesystem without them having to copy-and-pasting by hand. The harness is the final place where engineers can customize how coding agents do work before the LLM takes over. Think of the LLM as a train and the harness as the rails the train rides on. Below… one full task executed by a harness, traced step by step....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co