Skip to content

This Dot Blog

This Dot provides teams with technical leaders who bring deep knowledge of the web platform. We help teams set new standards, and deliver results predictably.

Newest First
Tags: TypeScript
How to configure and optimize a new Serverless Framework project with TypeScript cover image

How to configure and optimize a new Serverless Framework project with TypeScript

Elevate your Serverless Framework project with TypeScript integration. Learn to configure TypeScript, enable offline mode, and optimize deployments to AWS with tips on AWS profiles, function packaging, memory settings, and more....

Testing a Fastify app with the NodeJS test runner cover image

Testing a Fastify app with the NodeJS test runner

ant to simplify your testing process? Our new blog post on Node.js' built-in test runner is a great place to start. Learn how to test Fastify apps, including practical examples for testing an API server and SQL plugins....

Understanding Vue's Reactive Data cover image

Understanding Vue's Reactive Data

A blog post that unravel how Reactivity works in Vue 3...

Angular 17: Continuing the Renaissance cover image

Angular 17: Continuing the Renaissance

Dive into the Angular Renaissance with Angular 17, emphasizing standalone components, enhanced control flow syntax, and a new lazy-loading paradigm. Discover server-side rendering improvements, hydration stability, and support for view transitions....

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

How to host a full-stack app with AWS CloudFront and Elastic Beanstalk cover image

How to host a full-stack app with AWS CloudFront and Elastic Beanstalk

You have an SPA with a NestJS back-end. What if your app is a hit? You need to be prepared to serve thousands of users? You might need to scale your API horizontally, which means you need to have more instances running behind a load balancer....

Drizzle ORM: A performant and type-safe alternative to Prisma cover image

Drizzle ORM: A performant and type-safe alternative to Prisma

Introduction I’ve written an article about a similar, more well-known TypeScript ORM named Prisma in the past. While it is a fantastic library that I’ve used and have had success with personally, I noted a couple things in particular that I didn’t love about it. Specifically, how it handles relations with add-on queries and also its bulk that can slow down requests in Lambda and other similar serverless environments. Because of these reasons, I took notice of a newer player in the TypeScript ORM space named Drizzle pretty quickly. The first thing that I noticed about Drizzle and really liked is that even though they call it an ‘ORM’ it’s more of a type-safe query builder. It reminds me of a JS query builder library called ‘Knex’ that I used to use years ago. It also feels like the non-futuristic version of EdgeDB which is another technology that I’m pretty excited about, but committing to it still feels like a gamble at this stage in its development. In contrast to Prisma, Drizzle is a ‘thin TypeScript layer on top of SQL’. This by default should make it a better candidate for Lambda’s and other Serverless environments. It could also be a hard sell to Prisma regulars that are living their best life using the incredibly developer-friendly TypeScript API’s that it generates from their schema.prisma files. Fret not, despite its query-builder roots, Drizzle has some tricks up its sleeve. Let’s compare a common query example where we fetch a list of posts and all of it’s comments from the Drizzle docs: ` Sweet, it’s literally the same thing. Maybe not that hard of a sale after all. You will certainly find some differences in their APIs, but they are both well-designed and developer friendly in my opinion. The schema Similar to Prisma, you define a schema for your database in Drizzle. That’s pretty much where the similarities end. In Drizzle, you define your schema in TypeScript files. Instead of generating an API based off of this schema, Drizzle just infers the types for you, and uses them with their TypeScript API to give you all of the nice type completions and things we’re used to in TypeScript land. Here’s an example from the docs: ` I’ll admit, this feels a bit clunky compared to a Prisma schema definition. The trade-off for a lightweight TypeScript API to work with your database can be worth the up-front investment though. Migrations Migrations are an important piece of the puzzle when it comes to managing our applications databases. Database schemas change throughout the lifetime of an application, and the steps to accomplish these changes is a non-trivial problem. Prisma and other popular ORMs offer a CLI tool to manage and automate your migrations, and Drizzle is no different. After creating new migrations, all that is left to do is run them. Drizzle gives you the flexibility to run your migrations in any way you choose. The simplest of the bunch and the one that is recommended for development and prototyping is the drizzle-kit push command that is similar to the prisma db push command if you are familiar with it. You also have the option of running the .sql files directly or using the Drizzle API's migrate function to run them in your application code. Drizzle Kit is a companion CLI tool for managing migrations. Creating your migrations with drizzle-kit is as simple as updating your Drizzle schema. After making some changes to your schema, you run the drizzle-kit generate command and it will generate a migration in the form of a .sql file filled with the needed SQL commands to migrate your database from point a → point b. Performance When it comes to your database, performance is always an extremely important consideration. In my opinion this is the category that really sets Drizzle apart from similar competitors. SQL Focused Tools like Prisma have made sacrifices and trade-offs in their APIs in an attempt to be as database agnostic as possible. Drizzle gives itself an advantage by staying focused on similar SQL dialects. Serverless Environments Serverless environments are where you can expect the most impactful performance gains using Drizzle compared to Prisma. Prisma happens to have a lot of content that you can find on this topic specifically, but the problem stems from cold starts in certain serverless environments like AWS Lambda. With Drizzle being such a lightweight solution, the time required to load and execute a serverless function or Lambda will be much quicker than Prisma. Benchmarks You can find quite a few different open-sourced benchmarks of common database drivers and ORMs in JavaScript land. Drizzle maintains their own benchmarks on GitHub. You should always do your own due diligence when it comes to benchmarks and also consider the inputs and context. In Drizzle's own benchmarks, it’s orders of magnitudes faster when compared to Prisma or TypeORM, and it’s not far off from the performance you would achieve using the database drivers directly. This would make sense considering the API adds almost no overhead, and if you really want to achieve driver level performance, you can utilize the prepared statements API. Prepared Statements The prepared statements API in Drizzle allows you to pre-generate raw queries that get sent directly to the underlying database driver. This can have a very significant impact on performance, especially when it comes to larger, more complex queries. Prepared statements can also provide huge performance gains when used in serverless environments because they can be cached and reused. JOINs I mentioned at the beginning of this article that one of the things that bothered me about Prisma is the fact that fetching relations on queries generates additional sub queries instead of utilizing JOINs. SQL databases are relational, so using JOINs to include data from another table in your query is a core and fundamental part of how the technology is supposed to work. The Drizzle API has methods for every type of JOIN statement. Properly using JOINs instead of running a bunch of additional queries is an important way to get better performance out of your queries. This is a huge selling point of Drizzle for me personally. Other bells and whistles Drizzle Studio UIs for managing the contents of your database are all the rage these days. You’ve got Prisma Studio and EdgeDB UI to name a couple. It's no surprise that these are so popular. They provide a lot of value by letting you work with your database visually. Drizzle also offers Drizzle Studio and it’s pretty similar to Prisma Studio. Other notable features - Raw Queries - The ‘magic’ sql operator is available to write raw queries using template strings. - Transactions - Transactions are a very common and important feature in just about any database tools. It’s commonly used for seeding or if you need to write some other sort of manual migration script. - Schemas - Schemas are a feature specifically for Postgres and MySQL database dialects - Views -Views allow you to encapsulate the details of the structure of your tables, which might change as your application evolves, behind consistent interfaces. - Logging - There are some logging utilities included useful for debugging, benchmarking, and viewing generated queries. - Introspection - There are APIs for introspecting your database and tables - Zod schema generation - This feature is available in a companion package called drizzle-zod that will generate Zod schema’s based on your Drizzle tables Seeding At the time of this writing, I’m not aware of Drizzle offering any tools or specific advice on seeding your database. I assume this is because of how straightforward it is to handle this on your own. If I was building a new application I would probably provide a simple seed script in JS or TS and use a runtime like node to execute it. After that, you can easily add a command to your package.json and work it into your CI/CD setup or anything else. Conclusion Drizzle ORM is a performant and type-safe alternative to Prisma. While Prisma is a fantastic library, Drizzle offers some advantages such as a lightweight TypeScript API, a focus on SQL dialects, and the ability to use JOINs instead of generating additional sub queries. Drizzle also offers Drizzle Studio for managing the contents of your database visually, as well as other notable features such as raw queries, transactions, schemas, views, logging, introspection, and Zod schema generation. While Drizzle may require a bit more up-front investment in defining your schema, it can be worth it for the performance gains, especially in serverless environments....

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

Creating Custom Types in TypeScript with Indexed Access Types, Const Assertions, and Satisfies cover image

Creating Custom Types in TypeScript with Indexed Access Types, Const Assertions, and Satisfies

Frequently when writing TypeScript, you may need to create a new type from an existing type. For example, you may have a large type that you need to use in multiple places, and you want to create a new type that is a subset of the original type. Or you may have a large object full of data that you want to use to create types to maintain type safety. In this post, we'll cover how to create new types from existing types and data in TypeScript. Accessing parts of a type with indexed access types In JavaScript, you can access an object property's value with the string key of that property using someObject['someProperty']. You can use the same sort of syntax with TypeScript's types to get specific pieces out of a type. For example: ` Using TypeName["someProperty"] allows you to extract that piece of the type. These are called indexed access types. If you needed to use a piece of a large, complex type, you could simply pull that piece out into its own type using indexed access types. Why indexed access types? But what good is this? Couldn't I just refactor? In the previous example, wouldn't it be better for the pizza's Toppings to be a type of its own before defining Pizza, and then passed in as toppings: Toppings? I'd say yes, it would be. (And we'll cover that later!) But what if you're working with a type that you don't have control over (e.g., from a third party library), but you need to use a piece of it in a different type? That's where indexed access types come in. Why not just use Pick? Wait, why not just use Pick instead of indexed access types? You would want to use the indexed access type when you want _specifically_ a piece of the type, and not a type with that single property. For example: ` The index is a type! It isn't obvious from looking at the examples, but when you index a type, you're doing so with another type! So if I wanted to access a piece of a type with a defined string, it would fail. For example: ` In this case, I would instead have to use Pizza[typeof key] to get the same result as I would from just passing the value directly as Pizza["toppings"]. Alternatively, changing const key into type key would work. Because the index is a type, you can pass a type in as the index. This lets me do things like tell TypeScript: "I want to create a type that could be any one of the items in this array". You would do this by using the type number as your index access type. For example, if I wanted to create a single Topping type from our Pizza example, I could do the following: ` Creating types with const assertions Sometimes in TypeScript, you'll have some object full of data that you would like to use in a type-safe way. Let's return to our pizza example. Say we're building a web app to let people order our pizzas. Inside our order form, we have a list of toppings. This list of data could include a name, a description, and an extra price. ` Since we've gone through the trouble of writing all of this out, we should use this data to inform the Pizza type about our toppings. If we don't, it's both a duplication of code (a time-waster) and an opportunity for this data to get out of sync with our Pizza type. For a first attempt, you might use the indexed access types we learned about earlier to get each of the topping names: ` But that won't work! TypeScript has widened the type from those literal values to the broader string type. It doesn't assume that these values can't be changed later on. But it did notice that every name in TOPPINGS was a string, so it decided that the string type was the safest bet. Here, you can see how it would widely interpret the type of any entry in TOPPINGS: ` This is a good default, but it's not what we want here. The fix to this problem is easy: const assertions. We can simply append as const at the end of our TOPPINGS declaration. This tells TypeScript that we want to treat everything about this object as literal values that should not be widened. For example: ` Now we've got a type with all of the literal values from TOPPINGS as readonly properties in our type! From here, we can use indexed access types to create our Topping type from the name property: ` And we can use this type to inform our Pizza type: ` Extra type safety with satisfies Let's say we're factoring out the available crusts for making our Pizza. We could start with an array of strings, use a const assertion to use the literal values and avoid widening, and then again use our indexed access types to create a type from that array: ` Well, almost there. Notice that we have an undefined type in there. That's because we have an extra comma in our array. This is effectively the same as saying ['thin', 'thick', undefined, 'stuffed']. You could detect the undefined with type annotations, but that can't be mixed with const assertions. The type cannot both be string[] and readonly ['thin', 'thick', 'stuffed']. ` To avoid this issue, we can use satisfies to confirm that the value conforms to a certain intended shape. In our case, we want to confirm that the array is a tuple of strings. We don't need TypeScript to confirm which strings exactly—only that it matches the intended shape. ` We can further combine satifies with as const to get the literal values we want while verifying that the array is a tuple of strings: ` With as const, we tell TypeScript that it should not widen the inferred type of CRUSTS and that we expect it to be the literal values given. And with satisfies readonly string[], we tell TypeScript that CRUSTS should satisfy the shape of an array of readonly strings. Now we can't accidentally add an extra comma or other value to the array, and we can still use the literal values from CRUST to create new types. Conclusion The combination of indexed access types, const assertions, and the satisfies operator, give us a lot of power to create types that are more specific, and more accurate. You can use them to transform your data into useful types, rather than attempting to duplicate that information manually, and inevitably having the data and types fall out of sync. This can ultimately save you and your team a lot of time, effort, and headache. If you want to view the examples in this article in a runnable playground, you can find them at the TypeScript playground....

Exploring tRPC with Server-first UI Frameworks cover image

Exploring tRPC with Server-first UI Frameworks

If you’re a front-end engineer, you’re likely to see RPC (Remote Procedure Call) and new UI "meta-frameworks" popping up more and more, if you haven’t already....

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