catchFilter 配类型守卫,为什么错误类型收窄过头?
下面这段代码能通过编译,类型说残余错误只剩 Timeout;我实际运行,HttpError(404) 仍然以 HttpError 失败。类型说谎了。
原因是 Filter.fromPredicate 配类型守卫时,类型层按守卫类型整体做了 Exclude:守卫只证明「通过的是 HttpError」,却被当成「所有 HttpError 都通过」。规避办法是别让类型守卫承担额外的值条件。
这个坑不是 catchFilter 的 bug,而是类型守卫能表达的信息比你写进谓词里的条件少。catchFilter 与 Filter 的基本用法见 Effect.catchFilter 怎么同时匹配并提取错误?,下面只讲这个坑。
能编译却说谎的代码长什么样?
import { Data, Effect, Filter } from "effect"
class HttpError extends Data.TaggedError("HttpError")<{ readonly status: number }> {}
class Timeout extends Data.TaggedError("Timeout")<{ readonly ms: number }> {}
type Err = HttpError | Timeout
const call: Effect.Effect<string, Err> = Effect.fail(new HttpError({ status: 404 }))
const serverError = Filter.fromPredicate(
(e: Err): e is HttpError => e._tag === "HttpError" && e.status >= 500
)
export const program = call.pipe(Effect.catchFilter(serverError, () => Effect.succeed("recovered")))
export const lie: Effect.Effect<string, Timeout> = program谓词里写的是「HttpError 且状态码不小于 500」,但类型守卫的签名只能写成 e is HttpError。lie 这一行能通过编译,意味着类型认为 program 的错误只可能是 Timeout。运行时 HttpError(404) 不满足状态码条件,没有被恢复,以 HttpError 失败。
同样的问题还会出现在哪里?
Effect.catchIf 配带额外条件的类型守卫是同一个问题,我也跑过,结果一致。共同点是:守卫签名只描述类型,守卫函数体里的值条件在类型层面消失了。
怎么规避?
两种办法,都是不让类型守卫去承担「额外的值条件」:
- 用
Filter.tagged只按 tag 匹配,值条件放进处理器里,并用orElse重抛未处理的情况。Filter.tagged的残余错误类型是精确的,Filter.tagged<Err>()("Timeout")之后错误类型是HttpError | Decode。 - 像下面这样用
Filter.make,未命中分支返回Result.fail(e),Fail 类型保持完整输入类型,不会被收窄:
import { Effect, Filter, Result } from "effect"
import { call, type Err } from "./errors.ts"
export const notFound = Filter.make((e: Err) =>
e._tag === "HttpError" && e.status === 404 ? Result.succeed(e.body) : Result.fail(e)
)
export const program = call.pipe(
Effect.catchFilter(notFound, (body) => Effect.succeed(`not found: ${body}`))
)这里 program 的错误类型仍是 Err,不会被收窄成「除 404 以外」。这是诚实的:TypeScript 没法表达「不是 404 的 HttpError」,宁可保守也不要说谎。
那类型守卫式的 fromPredicate 什么时候可以用?
守卫条件和值条件完全等价时:比如只按 _tag 判断,守卫通过就等于整个成员都通过,Exclude 是对的。一旦谓词里还有状态码、字段值之类的额外条件,就别用守卫签名。我的做法是遇到值条件直接用 Filter.make。
以上基于 effect@4.0.5,lie那段代码我在本地编译并运行过,确认能通过编译且 404 仍以HttpError失败;catchIf配带额外条件的类型守卫也跑过,结果一致。「守卫与值条件等价时可以用」是我由此推出的结论,没有单独验证;规避办法里的notFound示例(errors.ts的定义见主文章)没有在这里再单独运行。