Someone finally fixed Javascript

Almaju - Apr 8 - - Dev Community

The JavaScript ecosystem evolves at a breakneck pace. Just as you get comfortable with a certain technique, a slew of new methodologies emerges. Some, like TypeScript, gain widespread adoption, while others, such as CoffeeScript, quietly fade away. Each innovation initially stirs excitement, but over time, the community often splits, with detractors eventually spawning their own frameworks. This endless cycle has made me increasingly wary of the latest "magic" frameworks purported to solve all problems. I've shifted from seeking tools as solutions to embracing the understanding of patterns over the constant chase for new technology.

This is why I'm pointing you towards something special for your TypeScript projects, not just another tool but a paradigm that encourages good practices: Effect.

Let's take a look at why you should take the leap.

Colored functions

Have you ever asked yourself what color is your function?

Let me summarize it for you. Imagine you have blue and red functions in your code base. The rule is simple: you can use red functions inside your blue functions but not the other way. Wouldn't that be a nightmare? Now replace blue by "async". Yep, you got function coloring in Javascript.

So how do we fight this coloring thing? If we want to remove colored functions, we would need to create some sort of wrapper that will use a Promise only when needed. Like "Future" or... "Effect"?

import { Effect, pipe } from "effect";

const fibonacci = (a: number): Effect.Effect<number> =>
  a <= 1
    ? Effect.succeed(a)
    : pipe(
        Effect.all([fibonacci(a - 1), fibonacci(a - 2)]),
        Effect.map(([a, b]) => a + b)
      );

await Effect.runPromise(fibonacci(10));
Enter fullscreen mode Exit fullscreen mode

The key difference when using Effect instead of Promise lies in how concurrency is handled. Effect provides fibers, which are lightweight concurrency structures similar to green threads or goroutines. This feature allows us to perform long-running or asynchronous tasks without blocking the main thread, which can be initiated even within traditionally synchronous functions.

import { Effect, Console, pipe } from "effect";

const longRunningTask = pipe(
  Console.log("Start of long running task"),
  Effect.delay(1000),
  Effect.tap(Console.log("End of long running task"))
);

console.log("Start of program");
Effect.runFork(longRunningTask);
console.log("End of program");

/**
 * OUTPUT:
 * Start of program
 * End of program
 * Start of long running task
 * End of long running task
 */
Enter fullscreen mode Exit fullscreen mode

While Effect does not eliminate the inherent async/sync distinctions (function coloring) in JavaScript, by using fibers to handle asynchronous operations, it allows synchronous functions to invoke asynchronous effects without becoming asynchronous themselves, thereby mitigating the "coloring" problem to a significant extent.

Typesafe errors

Let's look at this function:

const divide = (a: number, b: number) => a / b;
Enter fullscreen mode Exit fullscreen mode

We just introduced a problem here, we cannot divide by zero. So let's refactor the code a little bit:

const divide = (a: number, b: number) => {
  if (b === 0) throw new Error('Cannot divide by zero.');
  return a / b;
}
Enter fullscreen mode Exit fullscreen mode

Looks good to you? It is not. Because it is not typesafe. Someone that will want to use your function won't have any idea that your function can throw. This might look trivial with a simple function like this one but when you have dozens of potential errors, it can become a nightmare. Other more mature languages have notions such as Either or Result to have typesafe errors. It looks like that:

type Result<T, E> = Ok<T> | Err<E>;

// With something like:
type Ok<T> = { kind: "Ok", data: T };
type Err<E> = { kind: "Err", err: E };
Enter fullscreen mode Exit fullscreen mode

With Effect, you will have that out of the box: Effect<T, E>. You won't ever have to ask yourself what kind of errors can occur during the run, you can know it directly from the function signature. It also comes with helper functions to recover from errors.

const divide = (a: number, b: number): Effect<number, "DivisionByZeroError"> => {
  if (b === 0) return Effect.fail("DivisionByZeroError");
  return Effect.succeed(a / b);
}
Enter fullscreen mode Exit fullscreen mode

Newtypes or branded types

You know, looking back at my previous function I realize we could do better.

const divide = (a: number, b: NonZeroNumber) => ...
Enter fullscreen mode Exit fullscreen mode

How do you define NonZeroNumber though? If you just do type NonZeroNumber = number it won't prevent people to call it with "0". There is a pattern for that: newtypes. And yeah, Effect supports that too:

import { Brand } from "effect"

type NonZeroNumber = number & Brand.Brand<"NonZeroNumber">

const NonZeroNumber = Brand.refined<NonZeroNumber>(
  (n) => n !== 0, // Check if the value is a non-zero number
  (n) => Brand.error(`Expected ${n} to be a non-zero number`)
)
Enter fullscreen mode Exit fullscreen mode

This way, you know your function cannot be called with any number: it expects a special type of number which exclude zero.

Dependency injection

If you want to follow the "Inversion of Control" principle, you might want to look into "Dependency Injection". This concept is really simple: a function should have access to what it needs from its own context.

// With a singleton
const read = (filename) => FileReader.read(filename);

// With dependency injection
const read = (reader: FileReader) => (filename) => reader.read(filename);
Enter fullscreen mode Exit fullscreen mode

It's better to do it this way for many reasons such as uncoupling things, allowing for easy testing, having different contexts etc.

While several frameworks assist with this, Effect really crushed it by making it straightforward: put your dependencies as the third parameter of an Effect.

const read = (filename): Effect<File, Error, FileReader> => {
  return Effect.flatMap(FileReader, fileReader => {
    return fileReader.read(filename);
  })
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

There are many other reasons why you should consider Effect. For sure, it's not going to be easy at first, you will have to learn to code differently. But contrary to many frameworks that make you learn "their" way of doing something, Effect actually teaches you good patterns that have made their proofs in other languages. Actually, Effect is heavily inspired by ZIO from Scala which itself has been inspired by Haskell which is still today considered as one of the pinacle of good programming patterns.

. . . . . .