We now have multiple service required to run the final program.
Without Layer we are required to provide each service one by one:
export const program = Effect.gen(function* () {
const pokeApi = yield* PokeApi;
return yield* pokeApi.getPokemon;
});
const runnable = program.pipe(
Effect.provideService(PokeApi, PokeApi.Live),
Effect.provideService(PokemonCollection, PokemonCollection.Live),
Effect.provideServiceEffect(BuildPokeApiUrl, BuildPokeApiUrl.Live),
Effect.provideServiceEffect(PokeApiUrl, PokeApiUrl.Live)
);
provideServiceEffectis used to extract the service from anEffect.In our example
PokeApiUrlLiveandBuildPokeApiUrlLiveare defined usingEffect.gen, therefore we need to useprovideServiceEffectto extract the service fromEffect.
This strategy quickly becomes hard to maintain and read. Furthermore, if a service depends on another (interdependency), this will become even worst.
What Layer allows us to do instead is to organize all the dependencies and provide them only once.
A Layer for a service can be created using Layer.succeed:
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",
]);
}export class PokeApi extends Context.Tag("PokeApi")<PokeApi, typeof make>() {
static readonly Live = Layer.succeed(this, make);
}When a service is derived from an Effect instead we use Layer.effect:
Notice how we don't need
PokeApiUrl.ofandBuildPokeApiUrl.ofanymore, the type is inferred fromLayer.
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`;
})
);
}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}`;
})
);
}Each Layer is then composed using Layer.mergeAll:
const MainLayer = Layer.mergeAll(
PokeApi.Live,
PokemonCollection.Live,
BuildPokeApiUrl.Live,
PokeApiUrl.Live
);Notice how easy is to reference
Liveimplementations by making them astaticattribute on theclass
MainLayer is now a new Layer that contains all the dependencies defined inside mergeAll.
Finally, we can provide this single layer to run the final program using Effect.provide:
const MainLayer = Layer.mergeAll(
PokeApi.Live,
PokemonCollection.Live,
BuildPokeApiUrl.Live,
PokeApiUrl.Live
);
export const program = Effect.gen(function* () {
const pokeApi = yield* PokeApi;
return yield* pokeApi.getPokemon;
});
const runnable = program.pipe(Effect.provide(MainLayer));This allows to separate how we organize services (
Layer) from how we provide them (Effect.provide).We can use
Layerto manage each dependency while keeping a singleEffect.provideinsideindex.ts.
index.ts now doesn't contain any implementation details, but instead it defines how to compose layers and run the app:
import { Effect, Layer } from "effect";
import { BuildPokeApiUrl } from "./BuildPokeApiUrl";
import { PokeApi } from "./PokeApi";
import { PokeApiUrl } from "./PokeApiUrl";
import { PokemonCollection } from "./PokemonCollection";
const MainLayer = Layer.mergeAll(
PokeApi.Live,
PokemonCollection.Live,
BuildPokeApiUrl.Live,
PokeApiUrl.Live
);
export const program = Effect.gen(function* () {
const pokeApi = yield* PokeApi;
return yield* pokeApi.getPokemon;
});
const runnable = program.pipe(Effect.provide(MainLayer));
const main = runnable.pipe(
Effect.catchTags({
FetchError: () => Effect.succeed("Fetch error"),
JsonError: () => Effect.succeed("Json error"),
ParseError: () => Effect.succeed("Parse error"),
})
);
Effect.runPromise(main).then(console.log);We are not done yet. There is a type error!
Type 'PokeApiUrl' is not assignable to type 'never'.This is a missing dependency. What's happening here? Didn't we already provide PokeApiUrl to Layer.mergeAll?
const MainLayer = Layer.mergeAll(
PokeApi.Live,
PokemonCollection.Live,
BuildPokeApiUrl.Live,
PokeApiUrl.Live
);PokeApiUrl is there, but we missed something else: dependencies between layers. Let's fix this in the next lesson 👇
