We can notice a common pattern in all the services we defined:
makefunction that contains the implementation of the serviceContext.Tagclass for the service definitionLivelayer inside the service class as astaticattribute
// 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.Serviceallows to define the default service implementation (equivalent tomake) and the default layer (equivalent toLivelayer) 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"],
}
) {}
succeedis 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
PokemonCollectionis 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.genand 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.Serviceaccepts all types that can be assigned toimplementsin aclassdeclaration.// 👇 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.Serviceis specifically designed for service types, not for primitive values.
