Skip to content
Mattia Magi

AUTHOR

Mattia Magi

Senior Software Engineer

Select...
Select...
Next.js Route Groups cover image

Next.js Route Groups

Learn how to organize and optimize your application routing with ease. Say goodbye to messy routes and hello to a more intuitive and maintainable structure with the new Next.js Group Routes!...

Mastering Git Rerere: Solving Repetitive Merge Conflicts with Ease cover image

Mastering Git Rerere: Solving Repetitive Merge Conflicts with Ease

Are you curious to discover one of the hidden powers of Git? Incorporate git rerere into your Git workflow, and say goodbye to the frustration of repetitive merge conflicts....

Introducing TanStack Query v5: A Leap Forward in Simplicity and Functionality cover image

Introducing TanStack Query v5: A Leap Forward in Simplicity and Functionality

TanStack Query v5 is here! With simplified APIs, improved optimistic updates, shareable mutation state, first-class suspense support, streaming with React Server Components and much more....

Functional Programming in TypeScript using the fp-ts Library: Exploring Task and TaskEither Operators cover image

Functional Programming in TypeScript using the fp-ts Library: Exploring Task and TaskEither Operators

Introduction: Welcome back to our blog series on Functional Programming in TypeScript using the fp-ts library. In the previous three blog posts, we covered essential concepts such as the pipe and flow operators, Option type, and various methods and operators like fold, fromNullable, getOrElse, map, flatten, and chain. In this fourth post, we will delve into the powerful Task and TaskEither operators, understanding their significance, and exploring practical examples to showcase their usefulness. Understanding Task and TaskEither: Before we dive into the examples, let's briefly recap what Task and TaskEither are and why they are valuable in functional programming. Task: In functional programming, a Task represents an asynchronous computation that may produce a value or an error. It allows us to work with asynchronous operations in a pure and composable manner. Tasks are lazy and only start executing when we explicitly run them. They can be thought of as a functional alternative to Promises. Now, let's briefly introduce the Either type and its significance in functional programming since this concept, merged with Task gives us the full power of TaskEither. Either: Either is a type that represents a value that can be one of two possibilities: a value of type Left or a value of type Right. Conventionally, the Left type represents an error or failure case, while the Right type represents a successful result. Using Either, we can explicitly handle and propagate errors in a functional and composable way. Example: Handling Division with Either Suppose we have a function divide that performs a division operation. Instead of throwing an error, we can use Either to handle the potential division by zero scenario. Here's an example: ` In this example, the divide function returns an Either type. If the division is successful, it returns a Right value with the result. If the division by zero occurs, it returns a Left value with an error message. We then use the fold function to handle both cases, printing the appropriate message to the console. TaskEither: TaskEither combines the benefits of both Task and Either. It represents an asynchronous computation that may produce a value or an error, just like Task, but also allows us to handle potential errors using the Either type. This enables us to handle errors in a more explicit and controlled manner. Examples: Let's explore some examples to better understand the practical applications of Task and TaskEither operators. Example 1: Fetching Data from an API Suppose we want to fetch data from an API asynchronously. We can use the Task operator to encapsulate the API call and handle the result using the Task's combinators. In the example below, we define a fetchData function that returns a Task representing the API call. We then use the fold function to handle the success and failure cases of the Task. If the Task succeeds, we return a new Task with the fetched data. If it fails, we return a Task with an error message. Finally, we use the getOrElse function to handle the case where the Task returns None. ` Example 2: Performing Computation with Error Handling Let's say we have a function divide that performs a computation and may throw an error. We can use TaskEither to handle the potential error and perform the computation asynchronously. In the example below, we define a divideAsync function that takes two numbers and returns a TaskEither representing the division operation. We use the tryCatch function to catch any potential errors thrown by the divide function. We then use the fold function to handle the success and failure cases of the TaskEither. If the TaskEither succeeds, we return a new TaskEither with the result of the computation. If it fails, we return a TaskEither with an error message. Finally, we use the map function to transform the result of the TaskEither. ` In the first example, we saw how to fetch data from an API using Task and handle the success and failure cases using fold and getOrElse functions. This allows us to handle different scenarios, such as successful data retrieval or error handling when the data is not available. In the second example, we demonstrated how to perform a computation that may throw an error using TaskEither. We used tryCatch to catch potential errors and fold to handle the success and failure cases. This approach provides a more controlled way of handling errors and performing computations asynchronously. Conclusion: In this blog post, we explored the Task and TaskEither operators in the fp-ts library. We learned that Task allows us to work with asynchronous computations in a pure and composable manner, while TaskEither combines the benefits of Task and Either, enabling us to handle potential errors explicitly. By leveraging the concepts we have covered so far, such as pipe, flow, Option, fold, map, flatten, and chain, we can build robust and maintainable functional programs in TypeScript using the fp-ts library. Stay tuned for the next blog post in this series, where we will continue our journey into the world of functional programming....

Tag and Release Your Project with GitHub Actions Workflows cover image

Tag and Release Your Project with GitHub Actions Workflows

Tag and Release your project with GitHub Actions Workflows GitHub Actions is a powerful automation tool that enables developers to automate various workflows in their repositories. One common use case is to automate the process of tagging and releasing new versions of a project. This ensures that your project's releases are properly versioned, documented, and published in a streamlined manner. In this blog post, we will walk you through two GitHub Actions workflows that can help you achieve this. Understanding GitHub Tags and Releases GitHub tags and releases are essential features that help manage and communicate the progress and milestones of a project. Let's take a closer look at what they are, why they are useful, and how they can be used effectively. GitHub Tags A GitHub tag is a specific reference point in a repository's history that marks a significant point of development, such as a release or a specific commit. Tags are typically used to identify specific versions of a project. They are lightweight and do not contain any additional metadata by default. Tags are useful for several reasons: 1. Versioning: Tags allow you to assign meaningful version numbers to your project, making it easier to track and reference specific releases. 2. Stability: By tagging stable versions of your project, you can provide users with a reliable and tested codebase. 3. Collaboration: Tags enable contributors to work on specific versions of the project, ensuring that everyone is on the same page. GitHub Releases GitHub releases are a way to package and distribute specific versions of your project to users. A release typically includes the source code, compiled binaries, documentation, and release notes. Releases provide a convenient way for users to access and download specific versions of your project. Releases offer several benefits: 1. Communication: Releases allow you to communicate important information about the changes, improvements, and bug fixes included in a specific version. 2. Distribution: By packaging your project into a release, you make it easier for users to download and use your software. 3. Documentation: Including release notes in a release helps users understand the changes made in each version and any potential compatibility issues. Effective Use of Tags and Releases To make the most of GitHub tags and releases, consider the following tips: 1. Semantic Versioning: Follow a consistent versioning scheme, such as semantic versioning (e.g., MAJOR.MINOR.PATCH), to clearly communicate the nature of changes in each release. 2. Release Notes: Provide detailed and concise release notes that highlight the key changes, bug fixes, and new features introduced in each version. This helps users understand the impact of the changes and make informed decisions. 3. Release Automation: Automate the release process using workflows, like the one described in this blog post, to streamline the creation of tags and releases. This saves time and reduces the chances of human error. By leveraging GitHub tags and releases effectively, you can enhance collaboration, improve communication, and provide a better experience for users of your project. The Goal The idea is to have a GitHub action that, once triggered, updates our project's version, creates a new tag for our repository, and pushes the updates to the main branch. Unfortunately, the main branch is a protected branch, and it's not possible to directly push changes to a protected branch through a GitHub action. Therefore, we need to go through a pull request on the main branch, which, once merged, will apply the changes due to the version update to the main branch. We had to split the workflow into two different GitHub actions: one that creates a pull request towards the main branch with the necessary code changes to update the repository's version, and another one that creates a new tag and releases the updated main branch. This way, we have one additional click to perform (the one required to merge the PR), but we also have an intermediate step where we can verify that the version update has been carried out correctly. Let’s dive into these two workflows. Update version and create Release's PR Workflow ` Walkthrough: Step 1: Define the Workflow The workflow starts with specifying the workflow name and the event that triggers it using the on keyword. In this case, the workflow is triggered manually using the "workflow_dispatch" event, which means it can be run on-demand by a user. Additionally, the workflow accepts an input parameter called "version," which allows the user to specify the type of version bump (major, minor, or patch). The workflow_dispatch event allows the user to set the "version" input when running the workflow. Step 2: Prepare the Environment The workflow will run on an Ubuntu environment (ubuntu-latest) using a series of steps under the jobs section. The first job is named "version." Step 3: Checkout the Code The workflow starts by checking out the code of the repository using the actions/checkout@v3 action. This step ensures that the workflow has access to the latest codebase before making any modifications. Step 4: Set up Node.js Next, the workflow sets up the Node.js environment using the actions/setup-node@v3 action and specifying the Node.js version 16.x. It's essential to use the appropriate Node.js version required by your project to avoid compatibility issues. Step 5: Install Dependencies To ensure the project's dependencies are up-to-date, the workflow runs npm install to install the necessary packages as defined in the package.json file. Step 6: Configure Git To perform version bump and create a pull request, the workflow configures Git with a user name and email. This allows Git to identify the author when making changes in the repository. Step 7: Update the Version The workflow now performs the actual version bump using the npm version command. The new version is determined based on the "version" input provided when running the workflow. The updated version number is stored in an output variable named update_version, which can be referenced later in the workflow. Step 8: Update the Changelog After bumping the version, the workflow updates the CHANGELOG.md file to reflect the new release version. It replaces the placeholder "Unreleased" with the updated version using the sed command. [*We will return to this step later*] Step 9: Create a Pull Request Finally, the workflow creates a pull request using the peter-evans/create-pull-request@v5 action. This action automatically creates a pull request with the changes made in the workflow. The pull request will have a branch name following the pattern "release/", where corresponds to the updated version number. The outcome of this workflow will be a new open PR in the project with package.json and CHANGELOG.md file changed. [*we will speak about the changelog file later*] Now we can check if the changes are good, approve the PR and merge it into main. Merge a PR with a title that starts with "Release:" automatically triggers the second workflow Tag & Release Workflow ` Walkthrough: As you can see we added a check for the PR title before starting the job once the PR is merged and closed. Only the PRs with a title that starts with "Release:" will trigger the workflow. The first three steps are the same as the one described in the previous workflow: we check out the code from the repository, we set up node and we install dependencies. Let's start with: Step 4: Check formatting To maintain code quality, we run the npm run format:check command to check if the code adheres to the specified formatting rules. This step helps catch any formatting issues before proceeding further. Step 5: Build The npm run build command is executed in this step to build the project. This step is particularly useful for projects that require compilation or bundling before deployment. Step 6: Set up Git To perform Git operations, such as tagging and pushing changes, we need to configure the Git user's name and email. This step ensures that the correct user information is associated with the Git actions performed later in the workflow. Step 7: Get tag In this step, we retrieve the current version of the project from the package.json file. The version is then stored in an output variable called get_tag.outputs.version for later use. Step 8: Tag the commit Using the version obtained in the previous step, we create a Git tag for the commit. The tag is annotated with a message indicating the version number. Finally, we push the tag and associated changes to the repository. Step 9: Create changelog diff To generate release notes, we extract the relevant changelog entries from the CHANGELOG.md file. This step helps summarize the changes made since the previous release. (We will return to this step later) Step 10: Create release Using the actions/create-release action, we create a new release on GitHub. The release is associated with the tag created in the previous step, and the release notes are provided in the body of the release. Step 11: Delete release_notes file Finally, we delete the temporary release_notes.md file created in Step 9. This step helps keep the repository clean and organized. Once also the second workflow is finished our project is tagged and the new release has been created. The "Changelog Steps" As you can see the release notes are automatically filled, with a detailed description of what has been added, fixed, or updated in the project. This was made possible thanks to the "Changelog steps" in our workflows, but to use them correctly, we need to pay attention to a couple of things while developing our project. Firstly, to the format of the CHANGELOG.md file. This will be our generic template: But the most important aspect, in addition to keeping the file up to date during developments by adding the news or improvements we are making to the code under their respective sections, is that every time we start working on a new project release, we begin the paragraph with ## [Unreleased]. This is because, in the first workflow, the step related to the changelog will replace the word "Unreleased" with the newly created project version. In the subsequent workflow, we will create a temporary file (which will then be deleted in the latest step of the workflow), where we will extract the part of the changelog file related to the new version and populate the release notes with it. Conclusion Following these Tag and Release Workflows, you can automate the process of creating releases for your GitHub projects. This workflow saves time, ensures consistency, and improves collaboration among team members. Remember to customize the workflow to fit your project's specific requirements and enjoy the benefits of streamlined release management....

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

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

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. ` 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. ` 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. ` 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. ` 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. ` 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. ` 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!...

Functional Programming in TypeScript Using the fp-ts Library: Option cover image

Functional Programming in TypeScript Using the fp-ts Library: Option

Welcome back to our blog series on functional programming with fp-ts! In our previous post, we introduced the concept of functional programming and how it can be used to write more robust and maintainable code. Also we talked about the building block of fp-ts library: Pipe and Flow operators. Today, we're going to dive deeper into one of the most useful tools in the fp-ts toolbox: the Option type. ## What is an Option? In JavaScript, we often encounter situations where a value may or may not exist. For example, when we try to access a property of an object that may be null or undefined. This can lead to runtime errors and unexpected behavior. The Option type in fp-ts provides a way to handle these situations in a safe and predictable manner. An Option is a container that can hold either a value or nothing. It is represented by the Option type in fp-ts, which has two constructors: Some and None. The Some constructor is used to wrap a value, while the None constructor represents the absence of a value. 1. *some*: The some constructor is used to create an instance of Option when a value is present, or when a computation succeeds. It takes the value as its argument, and wraps it inside of the Option type. ` 2. *none*: The none constructor is used to create an instance of Option when a value is absent, or when a computation fails. It does not take any arguments; it simply represents the absence of a value. ` The some and none constructors help us avoid null and undefined errors by forcing us to handle both cases explicitly using functional programming techniques. But let's look at some examples: ` In this example, the getUser function returns a User object that may contain missing data. We use Option to represent the email and phone fields, which may or may not be present. We can then pattern match on the Option to safely handle the case where the field is missing. *Pattern matching is a powerful feature in functional programming that allows developers to match values against a set of patterns and execute corresponding code based on the match.* We are pattern matching Option when we do this in the example above: ` ## Why use Option? Using Option can help us avoid runtime errors and make our code more robust. By explicitly handling the absence of a value, we can prevent null or undefined errors from occurring. This can also make our code easier to reason about, as we don't have to worry about unexpected behavior caused by missing values. Option can also help us write more expressive code. By using Some and None instead of null or undefined, we can make our intentions clearer, and reduce the cognitive load on other developers who may be reading our code. Let's say we have a function findUserById that retrieves a user from a database based on their ID. If the user is found, the function returns the user object. But if the user is not found, it returns null. ` In this example, we explicitly check for null to determine if the user was found or not. This approach can lead to potential runtime errors if we forget to check for null in some parts of our code. Now, let's rewrite the same example using fp-ts Option: ` In this example, the findUserById function returns an Option, which can either be some(user) if the user is found, or none if the user is not found. Instead of manually checking for null, we use the fold method provided by fp-ts Option. If the user is found (some(user)), the second function inside fold is executed (right function), and if the user is not found (none), the second function is executed (left function). This approach ensures that we handle both cases explicitly, avoiding potential null-related errors. *The fold method is a fundamental operation provided by the Option type in fp-ts. It allows us to extract values from an Option instance by providing two functions: one for handling the case when the Option has a value (Some), and another for handling the case when the Option is empty (None).* It's important to note that Option in fp-ts is implemented as a discriminated union, meaning the some and none constructors are different variants of the same type. This enables the compiler to enforce exhaustiveness checking, ensuring that we handle both cases when using fold or other methods that require pattern matching. ## Conclusion: In this post, we've introduced the Option type in fp-ts and shown how it can be used to handle null or undefined values in a type-safe manner. We've also seen how Option can be used to represent optional values in a more self-documenting way. In future posts, we will explore the fold method in more detail with examples, along with other useful Option methods combined with other new fp-ts operators. These techniques will further enhance our functional programming skills and allow us to handle Option instances more effectively....

Functional Programming in TypeScript using the fp-ts library: Pipe and Flow Operator cover image

Functional Programming in TypeScript using the fp-ts library: Pipe and Flow Operator

Functional programming (FP) is an approach to software development that uses pure functions to help developers in trying to create maintainable and easy-to-test software. FP is notable for its ability to efficiently parallelize pure functions. This approach should lead to code that is easier to reason about and test, and can often result in more concise and maintainable code. ## What you will find in this post Reading about “pure functions” or “parallelizing” things could be scary. Everything seems very “mathematical” and “theoretical”, but don’t be afraid: the goal of this post is to try to explain in a really practical way (theory and math will pop out only when really necessary) how to bring these core concepts and approach of FP in your TS project using Giulio Canti’s library fp-ts. Pure and Impure functions Pure functions always produce the same output for the same input, regardless of the context in which it is called. Calling this type of function also has no side effects, meaning it does not modify any external state or interact with the outside world in any way. Here is an example of such pure function: ` This function takes an array of numbers, doubles each number in the array, and returns a new array with the doubled numbers. Here are some characteristics of this function that make it pure: 1. No side effects: This function doesn't modify any external state outside of its own scope. It only operates on the input array, and returns a new array with the transformed values. 2. Deterministic: Given the same input, this function will always return the same output. There are no random or unpredictable behaviors. 3. No dependencies: This function doesn't rely on any external state or dependencies. It only depends on the input that's passed to it. Because of these characteristics, this function is considered pure. It's predictable, testable, and doesn't introduce any unexpected behavior. A side effect occurs in a program when you insert external code into your function. This prevents the function from “purely” performing its task. An impure function in TypeScript is a function that, when called with the same arguments, may produce different results each time it is called. This is because an impure function can depend on mutable state or external factors, such as the current time, user input, or global variables, which can change between calls. Here's an example of an impure function in TypeScript: ` In this example, the incrementCounter function modifies the global counter variable every time it is called. Since the function's return value depends on the current value of counter, calling the function with the same argument multiple times may produce different results. For example, if we call the function like this: ` The function's return value changes each time we call it because the value of counter is being modified by each call. This makes incrementCounter an impure function. Note that impure functions can make it harder to reason about code since their behavior can be unpredictable. In general, it's best to minimize the use of impure functions and rely on pure functions, which have no side effects and always produce the same output for a given input. ## Actions, Calculations, and Data - Thinking like a functional programmer Let’s try to focus one moment on side effects. The definition says FP avoids side effects, but side effects are the very reason we run our software. The definition seems to imply that we must try to completely avoid them when in reality, we use side effects when we have to. Functional programmers know side effects are necessary yet problematic, so they have a lot of tools for working with them. A functional programmer first tries to classify the code into 3 general categories: - Actions - Calculations - Data Actions depends on when they are called or how many times they are called. Calculations and Data instead doesn’t depend on when or how many times they are called, the difference is only that calculations can be executed, data cannot; you don’t really know what calculation will do until you run it. Functional programmers prefer data to calculations and calculations to actions and they try to build code by applying and composing as many “calculations” as possible in order to work with "solid" data (but also trying to “control” actions too). ## Fp-Ts There are many famous functional programming languages such as Haskell, Scala, Elm, Erlang, Elixir etc. Fp-Ts provides developers with popular patterns and reliable abstractions from typed functional languages in TypeScript. In this post, we will focus on the first two building blocks of fp-ts: Pipe and Flow operators. ## Pipe Operator The pipe operator is a built-in function in fp-ts that allows you to compose functions from left to right. It takes a value as its first argument and any number of functions as subsequent arguments (that accept only one argument of the same type of the returning type of the preceding function). The output of each function becomes the input of the next function in the pipeline, and the final result is returned. ` In this example, we start with the number. Then increment it, double it, and finally decrement it. The pipe operator allows us to chain these functions together in a readable and concise way. Why use the pipe operator? The pipe operator is useful for a few reasons: 1. It makes code more readable. By chaining functions together with the pipe operator, it is easy to see the flow of data through the functions. 2. It makes code more concise. Without the pipe operator, we would have to nest function calls, which can be hard to read and understand. 3. It makes code more modular. By breaking down a problem into smaller, composable functions, we can write more reusable code. ## Flow Operator We could say that the pipe operator is our basic brick to build our ecosystem, flow is really similar but it allows you to define a sequence of functions separately and then apply them to a value later. The flow operator is also a built-in function in fp-ts, it takes any number of functions as arguments and returns a new function that applies each function in order. ` In the example above, we create two simple functions addOne and double that takes a number and return a number. We then use the flow function from fp-ts to compose the two functions. The flow function takes any number of functions as arguments and returns a new function that applies each function in sequence from left to right. By using the Flow operator, we don't need to explicitly define the type of the addOne and double functions' parameters. TypeScript can infer their types based on how they are used in the function composition. This makes the code shorter and more readable while still ensuring type safety. Pros of using the Flow operator in fp-ts: 1. Type inference: The Flow operator allows TypeScript to infer the types of function parameters, making the code shorter and more readable while still ensuring type safety. 2. Composability: The Flow function provided by fp-ts makes it easy to compose multiple functions into a single function. Cons of using the Flow operator in fp-ts: 1. Learning curve: If you are new to functional programming, the Flow operator can be challenging to understand and use correctly. 2. Debugging: Since the Flow operator relies on type inference, it can be challenging to debug issues related to type mismatches or incorrect function composition. ## Differences between pipe and flow The key difference between pipe and flow is in how they handle type inference. Pipe is better at inferring types, as it can use the output type of one function as the input type of the next function in the pipeline. Flow, on the other hand, requires explicit type annotations to be added to the composed functions. ## When to use Pipe or Flow Both pipe and flow are useful tools for composing functions and creating pipelines in a functional and declarative way. However, there are some situations where one might be more appropriate than the other. Pipe is a good choice when you need to perform a linear sequence of transformations on a value in left-to-right order. Flow is a good choice when you need to create a reusable function that encapsulates a series of transformations. Conclusion Fp-ts is a powerful library that provides many tools and abstractions for functional programming in TypeScript. The pipe and flow operators are just two of the many features that it offers. In this post we started to explore the building blocks of this library, learning their powers and their main differences. If you're interested in learning more about functional programming, explore some of these other concepts and check out the fp-ts documentation for more information, or wait for the next blog post here on This Dot Blog!...