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 toEffect<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 toA | null
orA | 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 toEffect<R, L>
Either
andOption
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:
Array
Record
Trie
(👈 I contributed to add this on effect myself)HashMap
HashSet
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