What's next in your effect journey

The services and patterns that we learned in the course cover everything that I used in most of my effect projects. These are the APIs that you will probably use more often:

  • Effect
  • Context
  • Layer
  • Data
  • Config
  • Logger

Nonetheless, we left out some other important services. Let's quickly review some of them now, so you know what to research next as you learn more about effect.

Option

The Effect<A, E, R> type itself already contains success value (A), errors (E), and dependencies (R). Nonetheless, there are cases where we are only interested in knowing if a value is found or not.

The Option<A> type is a composable solution for a value A that may be present (Some<A>) or missing (None<A>):

export type Option<A> = None<A> | Some<A>

Option<A> is equivalent to Effect<A, NoSuchElementException>

An example is getting an element in an array. Since we generally don't know if an element is present or not, methods like Array.get return Option (from the Array service in effect).

Option<A> can be seen as an alternative to A | null or A | undefined.

The main difference is that Option in effect has a more composable API that allows to more easily inspect, modify, and extract possible missing values.

Either

Either is a union type that usually represents a success (Right) or failure (Left):

export type Either<R, L = never> = Left<L, R> | Right<L, R>

Either<R, L> is equivalent to Effect<R, L>

Either and Option are used when a function has no effects.

Some examples are getting a value from an array or representing an error or success.

If your program includes any effect (for example reading a database, logging, sending an email) then you should use the equivalent Effect instead.

Since both Option and Either can be represented as Effect, you can return them in any function that expected an Effect (for example inside .gen):

/// 👇 `Effect<void, string | NoSuchElementException>`
const main = Effect.gen(function*() {
  yield* Option.some(10); // NoSuchElementException
  yield* Either.left("abc"); // string
})

Match

Match brings pattern matching to typescript.

A typical example is matching on a finite number of states:

type State = "Idle" | "Loading" | "Error" | "Success";

const MatchState = Match.type<State>().pipe(
  Match.when("Idle", () => 0),
  Match.when("Loading", () => 1),
  Match.when("Error", () => 2),
  Match.when("Success", () => 3),
  Match.exhaustive
);

By using Match.exhaustive we enforce at compile-time that we matched all possible State.

You can also use Match.orElse if you don't need to match all cases:

type State = "Idle" | "Loading" | "Error" | "Success";

const MatchState = Match.type<State>().pipe(
  Match.when("Idle", () => 0),
  Match.when("Loading", () => 0),
  Match.orElse(() => -1)
);

@effect/platform

We saw how to perform an API request using effect core. You may wonder: why should I do all this work for such a common use case as an API request?

Fear not! The effect ecosystem features many packages designed to provide solution to common use cases. @effect/platform is one of these packages.

@effect/platform provides APIs for things such as http requests, file system access, cookies, url params, and more.

For example, this code implements the same API request we defined in the course:

import {
  FetchHttpClient,
  HttpClient,
  HttpClientRequest,
  HttpClientResponse,
} from "@effect/platform";
import { Effect, flow } from "effect";
import { Pokemon } from "../schemas";

export const main = Effect.gen(function* () {
  const baseClient = yield* HttpClient.HttpClient;
  const pokeApiClient = baseClient.pipe(
    HttpClient.mapRequest(
      flow(
        HttpClientRequest.acceptJson,
        HttpClientRequest.prependUrl("https://pokeapi.co/api/v2")
      )
    )
  );

  return yield* pokeApiClient.get("/pokemon/squirtle");
}).pipe(
  Effect.flatMap(HttpClientResponse.schemaBodyJson(Pokemon)),
  Effect.scoped,
  Effect.provide(FetchHttpClient.layer)
);

This code snippet includes all the features that you would expect from a http request:

  • Define headers (HttpClientRequest.acceptJson)
  • Implement shared client with base url for the request (HttpClientRequest.prependUrl)
  • Perform GET request using client (pokeApiClient.get)
  • Runtime schema validation of the response (HttpClientResponse.schemaBodyJson)

On top of all that, you get all the benefits of effect: error handling, resource management, dependency injection, and more.

You don't need to recreate the wheel every time! The effect ecosystem of packages keeps growing:

  • SQL: everything around @effect/sql and related packages (e.g. sql-pg, sql-mysql2, sql-sqlite)
  • Testing: @effect/vitest
  • Cluster: durable workflows @effect/cluster

Data structures

Remember: effect is the standard library for typescript. As such it provides all sort of data structures:

All these data structures are immutable, type safe, and stack safe.

Advanced effect

Just to give you an idea of everything that effect has to offer.

These are the most used advanced services in effect:

  • Fiber
  • Schedule
  • Scope
  • Stream
  • Deferred
  • Metric
  • PubSub
  • Queue

We won't dive into any of these now because they each require complex examples to even show how they work, let alone how to use their full API. I encourage you to poke around the API to learn about these by yourself.

These would be the topic of a (possible future) effect advanced course