Effect.Service: Service and Layer all in one

We can notice a common pattern in all the services we defined:

  1. make function that contains the implementation of the service
  2. Context.Tag class for the service definition
  3. Live layer inside the service class as a static 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 to make) and the default layer (equivalent to Live 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 with Layer.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 requires Layer.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 to implements in a class 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.