DEV Community

Cover image for Breaking Down Effect TS : Part 1
Modgil
Modgil

Posted on

Breaking Down Effect TS : Part 1

If you're looking to develop applications that are type-safe, handle errors efficiently, manage concurrency effectively, and tackle complexity with improved code quality, all while applying functional programming principles, then Effect-TS is the ideal tool for you.

Effect-TS is a powerful TypeScript library designed to bring the principles and advantages of functional programming to the forefront of TypeScript development. It focuses on enabling developers to manage effects—such as state, errors, and asynchronous operations—in a functional, declarative, and type-safe manner.
Effect is inspired by ZIO (a Scala library)

Before diving into Effect-TS, let's explore what Functional Programming is and the problems it solves.

Functional Programming is a programming paradigm where functions are treated as first-class citizens. In this context, functions always yield the same result given the same input, highlighting the concept of immutability. Functional Programming enhances predictability and simplifies debugging through pure functions and immutability, ensuring consistent behaviour. It promotes higher modularity and reusability by encouraging the development of small, composable functions, which can be easily reused across various application modules. This leads to more robust, maintainable, and scalable software solutions compared to other paradigms.

Key Characteristics of Functional Programming:

- Immutability of Data/State:
This refers to the principle that data or state should not be modified after its creation, promoting stability and predictability in your code.

- Pure Functions:
These functions always produce the same output for the same input and have no side effects. They do not rely on any external state, ensuring deterministic behaviour.

Benefits of Pure Functions:

Predictability and Composability: Pure functions are predictable and can be easily composed to build complex logic.
Concurrency: Since pure functions do not modify external states, they are inherently thread-safe.

- Functions as First-Class Citizens:
In functional programming, functions are treated like any other value, meaning they can be assigned to variables, passed as arguments, returned from other functions, and stored in data structures. This enables the use of higher-order functions, which accept functions as arguments and return functions as output. For example, array.map() is a common higher-order function.

- Preference for Recursion Over Loops:
Functional programming prefers recursion to loops because recursion is more declarative and avoids mutable state by relying on the call stack to manage state. This approach avoids mutation by repeatedly calling the function with new arguments.

- Referential Transparency:
An expression is referentially transparent if it can be replaced with its corresponding return value without altering the program's behaviour. This improves code readability and reasoning.

How Effect-TS is aligned with Functional Programming Principles?
Effect-TS aligns with functional programming principles by emphasizing purity and immutability, managing side effects explicitly and predictably. It leverages TypeScript's type system to ensure type safety, reducing errors. By supporting effect composition and maintaining referential transparency, Effect-TS enables the creation of modular, reusable, and robust applications.

Apart from this Effect main feature includes(as per Effect-TS documentation):

Image description

Let's understand how to use Effect:

In Effect-TS, we use effects to manage a function's side effects. Normally, a function may either succeed or throw an exception, and handling exceptions can become challenging when managing numerous functions. Effect-TS provides a structured way to handle these scenarios by defining the return type of a function as an Effect.

An Effect is represented as Effect<Success, Error, Requirements>,
where:

Success is the type of value returned when the function succeeds.

Error is the type of error returned if the function fails.

Requirements represents the dependencies required by the function.

Effect<A, E, R> abstracts complex computations, allowing you to express operations that might fail, return a result, and need certain inputs. By handling both synchronous and asynchronous computations through this abstraction, Effect-TS streamlines functional programming in TypeScript, ensuring side effects are managed safely and predictably.

Effect functions return values of the effect type, enabling lazy execution. To execute and resolve the values, you need to explicitly run the effect after invoking the function.
This is different from Promises, which are initiated immediately as they are defined.

Let's start with the Basics:

The key advantage of using effects in Effect-TS is that they define computations without executing them immediately. This means you can set up your workflow and only execute it when you are fully prepared and satisfied with its setup. Think of it as mapping out a plan and only putting it into action when you're ready.

Creating Effects:
When we say:

export const succeed = Effect.succeed(5);

Enter fullscreen mode Exit fullscreen mode

Here, succeed creates an Effect that encapsulates its argument 5 in the success channel (A in Effect<A, E, R>).

Type of succeed is Effect.Effect<number, never, never>

This indicates it will produce a success of type number and cannot fail or require any environment.

export const fail = Effect.fail(3);

Enter fullscreen mode Exit fullscreen mode

fail creates an Effect that encapsulates its argument 3 in the failure channel (E in Effect<A, E, R>).

Type of fail is Effect.Effect<never, number, never>;

This denotes that it results in a failure of type number and does not produce a success or require any environment.

There are multiple ways to create effects in Effect-TS, whether for synchronous or asynchronous operations. To illustrate the creation of an asynchronous effect, let's consider the example of uploading a file to an S3 bucket.

export const uploadFileToS3 = (
  s3Client: S3Client,
  bucketName: string,
  key: string,
  body: Buffer | Uint8Array | Blob | string | ReadableStream
) =>
  Effect.tryPromise({
    try: () =>
      s3Client.send(
        new PutObjectCommand({
          Bucket: bucketName,
          Key: key,
          Body: body,
        })
      ),
    catch: (error) =>
      new Error(`Error uploading file to S3 bucket - ${bucketName}: ${error.message}`),
  });
Enter fullscreen mode Exit fullscreen mode

The uploadFileToS3 function creates an effect that attempts to upload a file to an S3 bucket using the provided S3Client, handling success with the upload command execution and capturing errors with a descriptive error message. It uses Effect.tryPromise to wrap the asynchronous operation, ensuring structured error handling and functional program flow.

Executing Effects:
As discussed earlier, up to this point, we have only constructed Effect values, and none of the computations we defined have been executed. Now, to execute the effects, just as there are various ways to create effects, there are also different ways to execute them. One such example of executing the above-created effect is:

Effect.runPromise(uploadFileToS3(s3Client, bucketName, key, fileContent));
Enter fullscreen mode Exit fullscreen mode

This executes the uploadFileToS3 effect using the provided inputs, initiating the file upload process to the specified S3 bucket.

I hope this introduction to Effect-TS has sparked your interest! There’s so much more to explore.
Stay tuned for the next part, where we'll dive deeper into hands-on examples of creating effects, exploring sophisticated error-handling techniques, running effects efficiently, leveraging concurrency mechanisms in Effect-TS, and covering more advanced topics.

Top comments (0)