We can notice a common pattern in all the services we defined:
make
function that contains the implementation of the serviceContext.Tag
class for the service definitionLive
layer inside the service class as astatic
attribute
// 1️⃣ `make` implementation
const make = /// ...
// 2️⃣ `Context.Tag` service
export class PokeApi extends Context.Tag("PokeApi")<
PokeApi,
Effect.Effect.Success<typeof make>
>() {
// 3️⃣ `Live` layer
static readonly Live = /// ...
}
Turns out this pattern is everywhere in effect, not just in the examples of this course. So common in fact that an API was introduced in v3.9 to simplify it all: Effect.Service
.
Effect.Service
allows to define the default service implementation (equivalent tomake
) and the default layer (equivalent toLive
layer) in a single class.
Non-effect services
Let's start by refactoring PokemonCollection
service.
import { Context, Layer, type Array } from "effect";
export class PokemonCollection extends Context.Tag("PokemonCollection")<
PokemonCollection,
Array.NonEmptyArray<string>
>() {
static readonly Live = Layer.succeed(this, [
"staryu",
"perrserker",
"flaaffy",
]);
}
Effect.Service
is similar to Context.Tag
in its definition:
- Defined using
class
- Requires to provide the service unique name (as a
string
) - Requires the first type parameter to be the service type itself
import { Effect } from "effect";
export class PokemonCollection extends Context.Tag("PokemonCollection")<
PokemonCollection,
Array.NonEmptyArray<string>
>() {}
export class PokemonCollection extends Effect.Service<PokemonCollection>()(
"PokemonCollection",
{ /* TODO */ }
) {}
Instead of adding Live
and providing it the default implementation, we can simply provide the default implementation in the second type parameter.
In this case, since the layer contains an Array
and not an Effect
, we can define succeed
:
import { Effect } from "effect";
export class PokemonCollection extends Effect.Service<PokemonCollection>()(
"PokemonCollection",
{
succeed: ["staryu", "perrserker", "flaaffy"],
}
) {}
succeed
is used for all the services where layers were previously defined withLayer.succeed
.
That's all you need with Effect.Service
to define a service.
Note that with this implementation the type of
PokemonCollection
is inferred as a constant array (readonly ["staryu", "perrserker", "flaaffy"]
).
Services defined using Effect
For BuildPokeApi
the only difference is that the service is implemented as Effect
:
import { Context, Effect, Layer } from "effect";
import { PokeApiUrl } from "./PokeApiUrl";
export class BuildPokeApiUrl extends Context.Tag("BuildPokeApiUrl")<
BuildPokeApiUrl,
({ name }: { name: string }) => string
>() {
static readonly Live = Layer.effect(
this,
Effect.gen(function* () {
const pokeApiUrl = yield* PokeApiUrl;
return ({ name }) => `${pokeApiUrl}/${name}`;
})
).pipe(Layer.provide(PokeApiUrl.Live));
}
Notice how the implementation uses
Effect.gen
and therefore requiresLayer.effect
.
In these cases, instead of succeed
the service is defined inside effect
:
import { Effect } from "effect";
import { PokeApiUrl } from "./PokeApiUrl";
export class BuildPokeApiUrl extends Effect.Service<BuildPokeApiUrl>()(
"BuildPokeApiUrl",
{
effect: Effect.gen(function* () {
const pokeApiUrl = yield* PokeApiUrl;
return ({ name }: { name: string }) => `${pokeApiUrl}/${name}`;
}),
}
) {}
Providing dependencies
We still need to provide the required dependencies to the service (PokeApiUrl
). Previously we used Layer.provide
directly:
export class BuildPokeApiUrl extends Context.Tag("BuildPokeApiUrl")<
BuildPokeApiUrl,
({ name }: { name: string }) => string
>() {
static readonly Live = Layer.effect(
this,
Effect.gen(function* () {
const pokeApiUrl = yield* PokeApiUrl;
return ({ name }) => `${pokeApiUrl}/${name}`;
})
).pipe(
Layer.provide(PokeApiUrl.Live),
);
}
Effect.Service
allows to do the same by defining dependencies
:
export class BuildPokeApiUrl extends Effect.Service<BuildPokeApiUrl>()(
"BuildPokeApiUrl",
{
effect: Effect.gen(function* () {
const pokeApiUrl = yield* PokeApiUrl;
return ({ name }: { name: string }) => `${pokeApiUrl}/${name}`;
}),
dependencies: [PokeApiUrl.Live],
}
) {}
Services retuning non-object values
Effect.Service
requires the service type to be an object.
Specifically,
Effect.Service
accepts all types that can be assigned toimplements
in aclass
declaration.// 👇 Anything that works here class Example implements Record<string, any> {}
In our example PokeApiUrl
returns a string
, which cannot be used with implements
:
export class PokeApiUrl extends Context.Tag("PokeApiUrl")<
PokeApiUrl,
string
>() {
static readonly Live = Layer.effect(
this,
Effect.gen(function* () {
const baseUrl = yield* Config.string("BASE_URL");
return `${baseUrl}/api/v2/pokemon`;
})
);
}
A class cannot implement a primitive type like 'string'.
It can only implement other named object types.ts(2864)
In these cases Effect.Service
cannot be used, and we still need to fall back to Context.Tag
.
Note that is not very common or realistic in practice to have a service that returns a non-object value.
Effect.Service
is specifically designed for service types, not for primitive values.