effect Schema 默认值错误通道升级:构造与解码默认值可携带 SchemaError 并沿路径传播
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
本文基于 t3code 仓库中 vendored 的 effect-smol(effect 的精简分支)源码展开。仓库根目录下
.repos/effect-smol/.changeset/pre/schema-defaults-issue-channel.md这份 changeset 记录了一次针对effect包的 patch 级行为变更:传递给 Schema 默认值系列 API 的Effect现在允许在错误通道中携带SchemaError,当默认值求值失败时,解析器会解包底层的SchemaIssue.Issue,并把它作为附带了所在字段路径的普通解析失败继续传播。读完本文,你将掌握withConstructorDefault与withDecodingDefault*系列 API 的默认值错误处理语义、SchemaError与底层 issue 的解包链路,以及如何用另一个 schema 的makeEffect/decode*直接充当默认值。
一、这次变更改了什么:changeset 原文解读
该变更记录位于 .changeset/pre/schema-defaults-issue-channel.md(处于pre预发布模式下,对应的 changeset 配置见 .changeset/config.json),要点如下:
- 变更级别:
"effect": patch,属于兼容性友好的行为增强,不破坏既有调用方式; - 受影响 API:
Schema.withConstructorDefault、Schema.withDecodingDefault、Schema.withDecodingDefaultKey、Schema.withDecodingDefaultType、Schema.withDecodingDefaultTypeKey五个默认值 API; - 新语义:传给这些 API 的
Effect允许在错误通道中产生SchemaError; - 失败传播方式:默认值失败时,解析器解包(unwrap)底层的
SchemaIssue.Issue,将其作为一次常规的解析失败向上传播,并自动附加上默认值所在的外围字段路径(surrounding path); - 直接收益:可以方便地把另一个 schema 的
makeEffect/decode*结果当作默认值,默认值的校验逻辑与主 schema 的解析体系无缝衔接。
二、Schema 默认值 API 全景:五个入口的职责划分
在 Schema.ts 中,五个 API 按"构造期 vs 解码期"和"字段级 vs 键级(key)"两条轴划分:
| API | 阶段 | 触发条件 | 默认值表示 | 错误通道 |
|---|---|---|---|---|
withConstructorDefault | 构造(make) | 构造时字段缺失 | Type(make 输入侧) | 可携带 Schema 解析错误,失败即解析失败 |
withDecodingDefault | 解码(decodeUnknown等) | 字段缺失或为undefined(值级) | Encoded表示 | 可携带SchemaError |
withDecodingDefaultKey | 解码 | 键缺失(不允许undefined,键级) | Encoded表示 | 可携带SchemaError |
withDecodingDefaultType | 解码 | 字段缺失或为undefined(值级) | Type表示(无需再经过解码变换) | 可携带SchemaError |
withDecodingDefaultTypeKey | 解码 | 键缺失(键级) | Type表示 | 可携带SchemaError |
从源码签名可确认这一划分:withDecodingDefaultKey与withDecodingDefault的默认值类型为Effect.Effect<S["Encoded"], SchemaError, R>(见 Schema.ts 与 Schema.ts),即默认值以编码侧表示给出,需要再走一遍解码变换;而withDecodingDefaultType的默认值类型为Effect.Effect<S["Type"], SchemaError, R>(见 Schema.ts),以解码后的 Type 表示直接给出,绕开解码变换;withDecodingDefaultTypeKey则在此之上把键设为解码侧可选。
三、核心机制:SchemaError 如何被解包并沿路径传播
变更的精髓在于错误通道的打通,底层实现位于toIssueEffect(见 Schema.ts):
function toIssueEffect<A, R>( self: Effect.Effect<A, SchemaError, R> ): Effect.Effect<A, SchemaIssue.Issue, R> { return Effect.catchCause(self, (cause) => Effect.failCauseSync(() => Cause_.map(cause, (error) => error.issue))) }它把错误通道中的每个SchemaError通过Cause_.map(cause, (error) => error.issue)剥离出底层的SchemaIssue.Issue(即"解包"),再交给外围解析器。外围解析器拿到该 issue 后,会像对待普通解析失败一样,把默认值所在的字段路径拼接到错误信息上——这就是 changeset 中"以带有所在路径的解析失败传播"的源码级含义。
而真正"何时执行默认值"的判断在 SchemaGetter.ts 的withDefault中:
export function withDefault<T, R = never>( defaultValue: Effect.Effect<T, SchemaIssue.Issue, R> ): Getter<T, T | undefined, R> { return new Getter((o) => { const filtered = Option.filter(o, Predicate.isNotUndefined) return Option.isSome(filtered) ? Effect.succeed(filtered) : Effect.mapEager(defaultValue, Option.some) }) }逻辑很直白:值不是undefined就直接放行;否则求值默认值 Effect。withConstructorDefault在 AST 层的组装(见 SchemaAST.ts)同样以SchemaGetter.withDefault作为解码变换、SchemaGetter.passthrough()作为编码变换,并把默认值以Link形式挂在字段上供构造期使用。
四、构造默认值:withConstructorDefault 的用法与新能力
withConstructorDefault的完整签名(见 Schema.ts):
export function withConstructorDefault<S extends Constraint & WithoutConstructorDefault>( defaultValue: Effect.Effect<S["~type.make.in"], SchemaIssue.Issue> ) { return (schema: S): withConstructorDefault<S> => make(SchemaAST.withConstructorDefault(schema.ast, defaultValue), { schema }) }它只接受满足WithoutConstructorDefault约束(尚无构造默认值)的 schema,且是柯里化风格。最简单的用法是构造一个静态默认值:
import { Effect, Schema } from "effect" const MySchema = Schema.Struct({ name: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed("anonymous"))) }) MySchema.make({}).name // => "anonymous"changeset 带来的新能力是:默认值不再局限于"直接成功的静态值",可以是任何会产生 Schema 解析错误的 Effect。最典型的场景是把另一个 schema 的decode*当作构造默认值——默认值本身要先通过校验,校验失败时错误被解包为底层 issue,并作为MySchema下对应字段的解析失败报告出来:
const schema = Schema.Struct({ updatedAt: Schema.DateFromString.pipe( Schema.withConstructorDefault( Schema.DateFromString.decodeUnknown("1970-01-01T00:00:00.000Z") ) ) }) // 构造时未提供 updatedAt:默认值经 DateFromString 解析后成功 schema.make({}) // => { updatedAt: Date("1970-01-01T00:00:00.000Z") }若默认值里放的是无法解析的字符串,make将抛出带updatedAt路径的解析失败,而不是静默吞掉错误。
五、解码默认值:键级与值级、Encoded 与 Type 的组合
解码期四个 API 的核心差异在于"缺失"的判定粒度和默认值的表示侧。以withDecodingDefaultKey为例,其 docblock 示例(见 Schema.ts):
const MySchema = Schema.Struct({ name: Schema.String.pipe(Schema.withDecodingDefaultKey(Effect.succeed("anonymous"))) }) Schema.decodeUnknownSync(MySchema)({}).name // => "anonymous"该 API 通过optionalKey(toEncoded(self))让键在编码侧可选(键可缺失但不可为undefined),默认值以Encoded表示给出,仍需经过解码变换。而withDecodingDefault对应值级(缺失或undefined都触发默认值),适合字段可能被显式传undefined的场景。
withDecodingDefaultType系列则以Type表示直接给出默认值,实现上先toType(self)再做withDecodingDefault,最后encodeTo(optional(self))(见 Schema.ts)——默认值不需要再经历一次解码,语义更贴近"字段本来就应该有这个值"。
这些 API 还统一支持encodingStrategy选项:
"passthrough"(默认):编码输出时保留该键值;"omit":编码输出时省略该键。
对应的编码变换在函数入口即根据选项选择SchemaGetter.omit()或SchemaGetter.passthrough()(见 Schema.ts),适合"默认值只影响解码、不应污染输出"的数据契约场景。
六、失败语义的边界:成功、解析失败与缺陷的分野
默认值 Effect 允许失败,但只允许以 Schema 解析错误的形式失败。这一点在 Schema.test.ts 中有明确的测试佐证:
- 正常路径:
Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1)))在make({})时返回{ a: -1 },构造失败则抛出错误(见 Schema.test.ts); - 解析失败(合法错误通道):
Effect.failCause(cause)产生的错误会按"解析失败"处理; - 缺陷(非法错误通道):若传入
Effect.die(new Error(...))这类非 Schema issue 的缺陷,测试断言make抛出"Constructor adapter can only throw schema issues",makeOption抛出"Option adapter can only return none for schema issues",且底层Cause.hasDies(...)为真(见 Schema.test.ts)。
这从反面印证了 changeset 的语义边界:错误通道被拓宽的是"Schema 体系内的解析错误",而非任意异常或缺陷。默认值函数内部若发生真正的程序缺陷,仍会被作为缺陷暴露,不会被误包装成输入校验错误。
七、实践指引与迁移建议
对正在使用或升级到该版本的开发者:
- 默认值可以"懒解析"了:当默认值本身有校验需求(如日期字符串、UUID、枚举校验),直接内联
另一个Schema.decodeUnknown(...)/makeEffect即可,无需手写catchAll再手工拼接路径——路径拼接由解析器自动完成。 - 按语义选 API:键级(Key)系列强调"键不存在",值级系列额外覆盖
undefined;Encoded系列默认值走完整解码管线,Type系列绕开解码变换,二者在默认值形态上二选一。 - 编码输出策略别忘配:需要"仅解码用默认值、编码时省略"时显式传
{ encodingStrategy: "omit" }。 - 兼容性:本次为 patch 级变更,原有
Effect.succeed(静态值)的用法完全不受影响;唯一需要注意的行为差异是——如果你的默认值 Effect 之前恰好通过错误通道返回了SchemaError(此前可能被视为意外错误),现在它会按带路径的解析失败正确传播。
八、小结
本次变更把 Schema 默认值的错误处理纳入统一的解析错误体系:默认值 Effect 的错误通道放宽为可携带SchemaError,失败时经toIssueEffect解包出底层SchemaIssue.Issue,再以带字段路径的解析失败继续传播。结合 SchemaGetter.withDefault 的undefined触发逻辑与 SchemaAST.withConstructorDefault 的变换组装,整个默认值机制现在既支持静态兜底值,也支持"由另一 schema 的解析结果充当默认值"这种组合式用法,让数据契约中的可选字段与校验逻辑在类型层面真正对齐。
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考