Effect, a powerful library for functional programming in JavaScript Effect, a powerful library for functional programming in JavaScript

Effect, a powerful library for functional programming in JavaScript

Effect is a powerful library that brings functional programming concepts to JavaScript, enabling developers to write cleaner, more maintainable code. It provides a set of tools and abstractions that make it easier to work with asynchronous operations, error handling, and side effects in a functional style.

A Brief Overview of Effect

Recently, I had the opportunity to work with Effect in a project where we started with just a small amount, and we were impressed by its capabilities. The library’s focus on immutability and pure functions aligns well with modern JavaScript practices, making it a great choice for developers looking to enhance their code quality. We ended up refactoring most of our code to use Effect, and the results were fantastic.

Effect’s approach to handling asynchronous operations is particularly noteworthy. It provides a powerful Effect type that allows you to compose and manage effects in a way that is both intuitive and efficient. This makes it easier to reason about your code and ensures that side effects are handled in a controlled manner.

If you’re interested in learning more about Effect and how it can improve your JavaScript development experience, I highly recommend checking out the official documentation. The library is actively maintained and has a growing community of developers who are passionate about functional programming in JavaScript.

How Effect changes the way I think about TypeScript

This is where I am going to pull out the code snippets that I have been using in my projects. I will also try to explain how Effect changes the way I think about TypeScript and how it can help you write better code.

Lets take the following example of a simple function that checks the HaveIBeenPwned database for a password:

example.ts
export async function checkPwnedDB(password: string): Promise<true | string> {
// Check if password is in pwned password database
const hash = encodeHexLowerCase(sha1(new TextEncoder().encode(password)));
const hashPrefix = hash.slice(0, 5);
const response = await fetch(`https://api.pwnedpasswords.com/range/${hashPrefix}`);
const data = await response.text();
const lines = data.split('\n');
for (const line of lines) {
const hashSuffix = line.slice(0, 35).toLowerCase();
if (hash === hashPrefix + hashSuffix) {
return 'Password must not be in the <a href="https://haveibeenpwned.com/Passwords" target="_blank">pwned password database</a>.';
}
}
// Password is strong/secure enough
return true;
}

This code make look simple, but it has a lot of potential issues. For example, if the fetch request fails, the function will throw an error and the user will not be able to see the error message. Also, if the response is not in the expected format, the function will throw an error and the user will not be able to see the error message.

With Effect, we can rewrite this code to handle errors and side effects in a more controlled manner:

example-effect.ts
const checkPwnedDB = (pass: string) =>
Effect.gen(function* () {
const client = yield* HttpClient.HttpClient;
const encodedData = new TextEncoder().encode(pass);
const sha1Hash = sha1(encodedData);
const hashHex = encodeHexLowerCase(sha1Hash);
const hashPrefix = hashHex.slice(0, 5);
const response = yield* client
.get(`https://api.pwnedpasswords.com/range/${hashPrefix}`)
.pipe(
Effect.catchTags({
RequestError: () => Effect.succeed({ text: Effect.succeed(''), status: 500 }),
ResponseError: () => Effect.succeed({ text: Effect.succeed(''), status: 500 }),
})
);
// If the API is unavailable, skip the check rather than failing
if (response.status >= 400) {
return;
}
const data = yield* response.text;
const lines = data.split('\n');
for (const line of lines) {
const hashSuffix = line.slice(0, 35).toLowerCase();
if (hashHex === hashPrefix + hashSuffix) {
return 'Password must not be in the <a href="https://haveibeenpwned.com/Passwords" target="_blank">pwned password database</a>.';
}
}
return;
});

This version of the function uses Effect’s gen function to create a generator function that can yield values. This allows us to handle asynchronous operations and errors in a more structured way. The catchTags method is used to catch specific error types, allowing us to handle them gracefully without throwing exceptions. This makes the code more robust and easier to maintain, as we can clearly see how errors are handled and what the expected behavior is in different scenarios.

The example above would have the following type

const checkPwnedDB: (pass: string) => Effect.Effect<string | undefined, ResponseError, never>

This type signature indicates that the function takes a string as input and returns an Effect that can either succeed with a string or be undefined (if the password is not found in the database) or fail with a ResponseError. This makes it clear what the function does and how it behaves, which is a key benefit of using Effect.

And this is just the beginning. Effect provides a wide range of utilities and types that can help you write better TypeScript code, making it easier to reason about your programs and ensuring that side effects are handled in a controlled manner.

Why Use Effect?

Effect is not just another library; it represents a shift in how we approach programming in JavaScript. By embracing functional programming concepts, Effect encourages developers to write code that is more predictable, easier to test, and less prone to bugs. Its focus on immutability and pure functions aligns well with modern development practices, making it a valuable tool for any JavaScript developer.

Lets give another example of how Effect can help us write better code. Consider a scenario where we need to hash a password.

hash.ts
import { scrypt as nodeScrypt } from 'node:crypto';
/**
* Removes the last element from a tuple type.
*
* @template T - The tuple type from which the last element will be removed.
* @typeParam T - A tuple type.
* @returns A new tuple type with the last element removed.
*
* @example
* ```typescript
* type Original = [1, 2, 3];
* type Modified = RemoveLast<Original>; // [1, 2]
* ```
*/
type RemoveLast<T extends unknown[]> = T extends [...infer Rest, infer _Last] ? Rest : never;
/**
* A wrapper function for the `nodeScrypt` function that returns a Promise.
* This function takes all the parameters of `nodeScrypt` except the last one (callback),
* and returns a Promise that resolves with the derived key or rejects with an error.
*
* @param {...RemoveLast<Parameters<typeof nodeScrypt>>} opts - The parameters for the `nodeScrypt` function, excluding the callback.
* @returns {Promise<Buffer>} A Promise that resolves with the derived key as a Buffer.
*/
function scrypt(...opts: RemoveLast<Parameters<typeof nodeScrypt>>): Promise<Buffer> {
return new Promise((res, rej) => {
nodeScrypt(...opts, (err, derivedKey) => {
if (err) rej(err);
else res(derivedKey);
});
});
}

This code defines a utility function scrypt that wraps the Node.js scrypt function, allowing us to use it in a Promise-based manner. This is a common pattern in JavaScript, but it can lead to callback hell and make the code harder to read and maintain. With Effect, we can rewrite this code to use the Effect type, which provides a more structured way to handle asynchronous operations:

hash-effect.ts
import { type ScryptOptions, scrypt } from 'node:crypto';
/**
* The `Scrypt` class provides a service for securely hashing passwords using the scrypt algorithm.
* It is implemented as an Effect service and depends on the `ScryptConfig` configuration layer.
*
* ### Usage
* The service exposes a function that takes a password as input and returns a derived key as a `Buffer`.
* The hashing process is asynchronous and uses the `Effect` framework for error handling and logging.
*
* ### Dependencies
* - `ScryptConfig.Layer`: Provides the configuration for the scrypt algorithm, including salt, key length, and options.
*
* ### Logging
* - Logs are generated using the `genLogger` and `pipeLogger` utilities for tracing the execution flow.
*
* ### Errors
* - If an error occurs during the hashing process, it is wrapped in a `ScryptError` and handled using the `Effect.fail` mechanism.
*
* @class Scrypt
* @extends Effect.Service
* @template Scrypt
* @memberof studiocms/lib/auth/utils/scrypt
*/
export class Scrypt extends Effect.Service<Scrypt>()('studiocms/lib/auth/utils/scrypt/Scrypt', {
effect: Effect.gen(function* () {
const { salt, keylen, options } = yield* ScryptConfig;
const run = (password: string) =>
Effect.async<Buffer, ScryptError>((resume) => {
const req = scrypt(password, salt, keylen, options, (error, derivedKey) => {
if (error) {
const toFail = new ScryptError({ error });
resume(Effect.fail(toFail));
} else {
resume(Effect.succeed(derivedKey));
}
});
return req;
});
return { run };
}),
dependencies: [ScryptConfig.Layer],
}) {}

This version of the Scrypt class uses Effect’s gen function to create a service that can be used to hash passwords. The run method is an asynchronous function that returns an Effect that can either succeed with a derived key or fail with a ScryptError. This makes it clear what the function does and how it behaves, which is a key benefit of using Effect.

While there is more code not shown here that is included in this portion of the file, the key takeaway is that Effect allows us to handle asynchronous operations and errors in a more structured way, making the code more robust and easier to maintain with proper error handling and logging.

The type signature for the run method would look like this:

const run: (password: string) => Effect.Effect<Buffer<ArrayBufferLike>, ScryptError, never>

This type signature indicates that the method takes a string as input (the password) and returns an Effect that can either succeed with a Buffer or fail with a ScryptError. This makes it clear what the method does and how it behaves.

Why I will continue using Effect

As I continue to explore the capabilities of Effect, I find that it not only enhances my TypeScript development experience but also encourages a more disciplined approach to coding. The library’s focus on immutability, pure functions, and controlled side effects aligns perfectly with the principles of functional programming, which can lead to cleaner and more maintainable code. Also, the growing community and active development around Effect mean that there are plenty of resources and support available for developers looking to adopt this library.

I have also loved the way Effects context works, allowing you to pass around dependencies and configurations in a way that is both flexible and type-safe. This makes it easy to manage complex applications without losing track of what dependencies are being used where and without having to pass them down a long chain of function calls.

Conclusion

If you’re looking to improve your TypeScript development experience and embrace functional programming concepts, I highly recommend giving Effect a try. Its powerful abstractions and focus on immutability and pure functions can help you write cleaner, more maintainable code. Plus, the growing community and active development mean that you’ll have access to a wealth of resources and support as you explore this library.

Learn more about Effect:

Read my post on StudioCMS’s Blog for more insights on how Effect can enhance your JavaScript projects.


← Back to blog