使用Schema.Class定义Schema
https://github.com/typeonce-dev/effect-getting-started-course
如果你在 IDE 中检查 program 的成功类型,你实际上看不到 Pokemon,而是看到一个包含我们在 Pokemon 内部定义的属性的对象。

成功类型包含完整的 Pokemon interface 及其所有属性
这个问题源于我们定义 Schema 的方式。使用 Schema.Struct 我们无法获得不透明类型。
不透明类型是一种底层结构被隐藏的类型。它就像一个黑盒子:你知道它代表什么,但不知道其内部细节。
使用 Schema.Struct 我们反而得到了类型的完整结构!
使用class和Schema.Class定义Schema
我们可以使用 class 和 Schema.Class,这允许同时定义形状并导出不透明类型:
定义一个继承
Schema.Class的class类型参数与
class名称相同(<Pokemon>)string参数是 Schema 的_tag第二个参数是 Schema 的形状
class Pokemon extends Schema.Class<Pokemon>("Pokemon")({ // 👇 参数与 `Schema.Struct` 相同 id: Schema.Number, order: Schema.Number, name: Schema.String, height: Schema.Number, weight: Schema.Number, }) {}
现在 Pokemon 可以直接用作类型:
class Pokemon extends Schema.Class<Pokemon>("Pokemon")({ id: Schema.Number, order: Schema.Number, name: Schema.String, height: Schema.Number, weight: Schema.Number, }) {} const extractId = (pokemon: Pokemon) => pokemon.id;
由于我们现在使用的是 class,我们也可以为其附加方法:
class Pokemon extends Schema.Class<Pokemon>("Pokemon")({ id: Schema.Number, order: Schema.Number, name: Schema.String, height: Schema.Number, weight: Schema.Number, }) { public get formatHeight(): string { return `${this.height}cm`; } }
我们还解决了 IDE 中不透明类型的问题。使用 Schema.Class 时,当你检查响应类型时,你会看到 Pokemon 而不是所有属性:

Schema.Class 定义了不透明类型,使 IDE 中的类型更易读
我在可能的情况下总是使用 Schema.Class 来定义我的 Schema。
Schema.Class不能用于非对象 Schema,如联合类型或原始类型。
/// 联合值使用不带 `Class` 的 `Schema.Literal` const PokemonType = Schema.Literal("fire", "water", "grass"); /// ⛔️ 不能使用 `Schema.Class` ⛔️ class PokemonType extends Schema.Class<PokemonType>("PokemonType")(Schema.Literal("fire", "water", "grass")) {}
这是我们现在的应用:
index.ts
import { Schema } from "effect"; import { Data, Effect } from "effect"; /** Schema 定义 **/ class Pokemon extends Schema.Class<Pokemon>("Pokemon")({ id: Schema.Number, order: Schema.Number, name: Schema.String, height: Schema.Number, weight: Schema.Number, }) {} /** 错误类型 **/ class FetchError extends Data.TaggedError("FetchError")<{}> {} class JsonError extends Data.TaggedError("JsonError")<{}> {} /** 实现 **/ const fetchRequest = Effect.tryPromise({ try: () => fetch("https://pokeapi.co/api/v2/pokemon/garchomp/"), catch: () => new FetchError(), }); const jsonResponse = (response: Response) => Effect.tryPromise({ try: () => response.json(), catch: () => new JsonError(), }); const decodePokemon = Schema.decodeUnknown(Pokemon); const program = Effect.gen(function* () { const response = yield* fetchRequest; if (!response.ok) { return yield* new FetchError(); } const json = yield* jsonResponse(response); return yield* decodePokemon(json); }); /** 错误处理 **/ const main = program.pipe( Effect.catchTags({ FetchError: () => Effect.succeed("Fetch error"), JsonError: () => Effect.succeed("Json error"), ParseError: () => Effect.succeed("Parse error"), }) ); /** 运行 Effect **/ Effect.runPromise(main).then(console.log);
当我们运行这个程序时,我们得到以下结果:
> effect-getting-started-course@1.0.0 dev > tsx src/index.ts { id: 445, order: 570, name: 'garchomp', height: 19, weight: 950 }
我们在原始的纯 TypeScript 解决方案基础上增加了相当多的代码行。尽管如此,这使我们能够从”快乐路径”转向完整的错误处理和 Schema 验证。
此外,这个应用是完全类型安全的。没有什么会在我们不知情的情况下出错,因为编译器会报告任何错误并阻止应用启动。
这太棒了!在实践中,这意味着不再有运行时错误。
我们有一句话:“如果能编译,就能工作”。
我们还不满足。我们仍在硬编码像 API url(https://pokeapi.co)这样的值:
const fetchRequest = Effect.tryPromise({ try: () => fetch("https://pokeapi.co/api/v2/pokemon/garchomp/"), catch: () => new FetchError(), });
这在测试和维护应用时会造成问题:
我们如何为测试更改 url?
我们不想每次使用时都复制粘贴 url
如果 url 因任何原因发生变化,很难重构
这使得随着应用规模的扩大,测试和组织代码变得困难。有一个解决方案:环境变量。
让我们看看如何在 Effect 中使用 Config 来管理它们!