1017 words
5 minutes
[Effect Services] 06. 使用Context.Tag定义服务

使用Context.Tag定义服务#

https://github.com/typeonce-dev/effect-getting-started-course

WARNING

GenericTag 的局限性

使用 GenericTag 定义服务时存在一些隐藏问题:

  • 我们正在定义、引用和导出单独的值(维护复杂且更容易出错)
/// 1️⃣ 定义服务接口 export interface PokeApi { readonly getPokemon: Effect.Effect< Pokemon, FetchError | JsonError | ParseResult.ParseError | ConfigError >; } /// 2️⃣ 为服务定义 `Context` export const PokeApi = Context.GenericTag<PokeApi>("PokeApi"); /// 3️⃣ 定义实现 export const PokeApiLive = PokeApi.of({ getPokemon: Effect.gen(function* () { const baseUrl = yield* Config.string("BASE_URL"); const response = yield* Effect.tryPromise({ try: () => fetch(`${baseUrl}/api/v2/pokemon/garchomp/`), catch: () => new FetchError(), }); if (!response.ok) { return yield* new FetchError(); } const json = yield* Effect.tryPromise({ try: () => response.json(), catch: () => new JsonError(), }); return yield* Schema.decodeUnknown(Pokemon)(json); }), });

index.ts

import { Effect } from "effect"; // 👇 为服务定义和实现分别/多次导入 import { PokeApi, PokeApiLive } from "./PokeApi"; const program = Effect.gen(function* () { const pokeApi = yield* PokeApi; return yield* pokeApi.getPokemon; }); const runnable = program.pipe(Effect.provideService(PokeApi, PokeApiLive));
  • 我们面临服务类型之间冲突的风险(具有相同结构的服务)
export interface PokeApi { readonly getPokemon: Effect.Effect< Pokemon, FetchError | JsonError | ParseResult.ParseError | ConfigError >; } // ⛔️ 这是 2 个不同的导出,但它们引用相同的 `PokeApi` 接口 export const PokeApi1 = Context.GenericTag<PokeApi>("PokeApi1"); export const PokeApi2 = Context.GenericTag<PokeApi>("PokeApi2");

如果我们想要 100% 确保避免冲突,我们需要添加另一个 interface,使用 Context.GenericTag第一个类型参数使每个服务唯一:

// 👇 `Symbol` 使这个实例唯一 interface _PokeApi1 { readonly _: unique symbol; } export const PokeApi1 = Context.GenericTag<_PokeApi1, PokeApi>("PokeApi1"); // 👇 `Symbol` 使这个实例唯一 interface _PokeApi2 { readonly _: unique symbol; } export const PokeApi2 = Context.GenericTag<_PokeApi2, PokeApi>("PokeApi2");

当然,Effect 为此提供了解决方案:classContext.Tag

使用Context.Tag的服务类#

我们可以使用 classContext.Tag 更轻松地定义服务:

  • 服务标签 “PokeApi”

  • 第一个类型参数与 class 名称相同(PokeApi

  • 第二个类型参数是服务的签名(PokeApiImpl

interface PokeApiImpl { readonly getPokemon: Effect.Effect< Pokemon, FetchError | JsonError | ParseResult.ParseError | ConfigError >; } export class PokeApi extends Context.Tag("PokeApi")<PokeApi, PokeApiImpl>() {}
TIP

Context.Tag 的优势

我们将完整的服务定义简化为一个值

  • 由于 PokeApi 是一个 class,它既可以作为值也可以作为类型

  • Context.Tag 确保服务是唯一的(内部)

  • 我们可以在 class 内部定义方法和属性,这些将对服务的任何实例都可访问。我们使用这个来将服务实现定义为 static 属性

interface PokeApiImpl { readonly getPokemon: Effect.Effect< Pokemon, FetchError | JsonError | ParseResult.ParseError | ConfigError >; } export class PokeApi extends Context.Tag("PokeApi")<PokeApi, PokeApiImpl>() { static readonly Live = PokeApi.of({ getPokemon: Effect.gen(function* () { const baseUrl = yield* Config.string("BASE_URL"); const response = yield* Effect.tryPromise({ try: () => fetch(`${baseUrl}/api/v2/pokemon/garchomp/`), catch: () => new FetchError(), }); if (!response.ok) { return yield* new FetchError(); } const json = yield* Effect.tryPromise({ try: () => response.json(), catch: () => new JsonError(), }); return yield* Schema.decodeUnknown(Pokemon)(json); }), }); }

Context.Tag 内部的 static readonly Live 是 Effect 中常见且推荐的模式。

它允许直接从 PokeApi 引用所有实现(LiveTestMock只需单次导入

有了这个,我们只需导入 PokeApi

index.ts

import { Effect } from "effect"; import { PokeApi } from "./PokeApi"; const program = Effect.gen(function* () { const pokeApi = yield* PokeApi; return yield* pokeApi.getPokemon; }); const runnable = program.pipe(Effect.provideService(PokeApi, PokeApi.Live)); 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);

使用 Context.Tag 而不是 Context.GenericTag最佳实践


再次,我们为一个开始时”简单”的 API 请求添加了大量代码。

PokeApi.ts

import { Config, Context, Effect, Schema, type ParseResult } from "effect"; import type { ConfigError } from "effect/ConfigError"; import { FetchError, JsonError } from "./errors"; import { Pokemon } from "./schemas"; interface PokeApiImpl { readonly getPokemon: Effect.Effect< Pokemon, FetchError | JsonError | ParseResult.ParseError | ConfigError >; } export class PokeApi extends Context.Tag("PokeApi")<PokeApi, PokeApiImpl>() { static readonly Live = PokeApi.of({ getPokemon: Effect.gen(function* () { const baseUrl = yield* Config.string("BASE_URL"); const response = yield* Effect.tryPromise({ try: () => fetch(`${baseUrl}/api/v2/pokemon/garchomp/`), catch: () => new FetchError(), }); if (!response.ok) { return yield* new FetchError(); } const json = yield* Effect.tryPromise({ try: () => response.json(), catch: () => new JsonError(), }); return yield* Schema.decodeUnknown(Pokemon)(json); }), }); }

index.ts

import { Effect } from "effect"; import { PokeApi } from "./PokeApi"; const program = Effect.gen(function* () { const pokeApi = yield* PokeApi; return yield* pokeApi.getPokemon; }); const runnable = program.pipe(Effect.provideService(PokeApi, PokeApi.Live)); 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);

Effect Playground

事实证明,服务仍然可能过于有限。如果我们有更多相互依赖的服务会怎样?

我们不想多次创建它们或在任何地方手动使用 Effect.provideService

欢迎使用 Layer!让我们跳到下一个模块来学习如何使用它!

[Effect Services] 06. 使用Context.Tag定义服务
https://0bipinnata0.my/posts/course/effect-beginners-complete-getting-started/effect-services/06-define-services-with-context-tag/
Author
0bipinnata0
Published at
2025-08-30 17:48:36