Effect Predicate 模块实战:运行时类型守卫与组合式校验指南
2026/9/15 15:43:48 网站建设 项目流程

Effect Predicate 模块实战:运行时类型守卫与组合式校验指南

【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code

导读

本文基于 effect-smol 仓库的 AI 教程文档 10_predicate/index.md 及其配套示例,系统讲解 Effect 标准库中Predicate模块的用法:什么是运行时类型守卫(Predicate / Refinement)、为什么团队规范要求"永远不要自己手写isRecordisString之类的辅助函数"、以及如何用and/or/not/compose等 API 把零散检查组合成精确的校验逻辑。读完本文,你将掌握一套复用、组合、类型安全的运行时校验方案,并能在处理unknown数据(如JSON.parse、外部 API 响应、用户输入)时写出简洁可靠的守卫代码。


一、Predicate 模块是什么

Predicate模块位于 packages/effect/src/Predicate.ts(约 1880 行),其模块级注释定义得很清楚:

Defines runtime checks for values. APredicate<A>returnstrueorfalsefor anA. ARefinement<A, B>is a predicate that also narrows the TypeScript type when it succeeds.

也就是说,该模块提供两类核心类型:

1.1Predicate<A>:只判断、不收窄

export interface Predicate<in A> { (a: A): boolean }

源码见 Predicate.ts。一个Predicate<A>是一个纯函数:对给定的A返回truefalse,自身不抛异常,也不会在类型层面收窄输入。它适合用作可复用的布尔判断,特别是要传给数组filter、迭代器或与其他谓词组合的场景。

1.2Refinement<A, B>:判断且收窄类型

export interface Refinement<in A, out B extends A> { (a: A): a is B }

源码见 Predicate.ts。Refinement是"带有类型收窄的谓词":当它返回true时,TypeScript 会把输入从A收窄为B。这是处理unknown值的核心武器——搭配iffilter使用,编译器就能在分支内安全访问具体字段。

示例(来自源码 JSDoc):

import { Predicate } from "effect" const isString: Predicate.Refinement<unknown, string> = (u): u is string => typeof u === "string" const data: unknown = "hello" if (isString(data)) { data.toUpperCase() // => "HELLO",类型已收窄为 string }

二、核心规范:不要自己写isRecord/isString

原文档用一句近乎强制的规范点明了本模块的定位:

NEVERwrite your own helper functions likeisRecordorisString, instead use the helpers from thePredicatemodule.

翻译过来就是:永远不要自己手写isRecordisString这类辅助函数,直接使用Predicate模块提供的守卫。原因很实际:

  1. 正确性有保障Predicate内置守卫经过完整测试覆盖,例如isObject要同时排除null和数组,这些边界条件极易在手写时遗漏;
  2. 类型安全:内置守卫大多以Refinement形式导出,能正确收窄类型,而随手写的typeof x === "string"未必带类型谓词标注;
  3. 可组合性:模块内守卫的类型签名(Refinement<unknown, T>)是为组合 API 量身设计的,自写函数类型各异,难以接入and/or/compose
  4. 避免重复代码:多模块项目里每个人各写一份isObject,语义可能不一致,维护成本高。

需要说明的是:原文档举例的isRecord在当前仓库源码中并无同名导出,当前版本里"非null、非数组的对象"检查由 isObject 承担,实现为:

export function isObject(input: unknown): input is { [x: PropertyKey]: unknown } { return typeof input === "object" && input !== null && !Array.isArray(input) }

注意这一行同时完成了三件事:排除typeof"object"null、排除数组、并收窄到可索引对象类型。类似的还有isObjectOrArray(L1010)和isObjectKeyword(L1105)。文档以isRecord/isString为例要传达的精神是一致的:运行时检查交给标准库,不造轮子


三、内置守卫一览:把"每个值的检查"都交给标准库

Predicate模块为 JavaScript 常见值提供了丰富的现成守卫(category: guards/predicates),全部定义于 Predicate.ts。常用列表如下:

守卫判断依据源码位置
isStringtypeof input === "string"L556
isNumbertypeof input === "number"L589
isBooleantypeof input === "boolean"L622
isBigInttypeof input === "bigint"L654
isSymboltypeof input === "symbol"L686
isPropertyKeystring \| number \| symbolL721
isFunctiontypeof input === "function"L753
isUndefinedinput === undefinedL784
isNullinput === nullL844
isNullishnull \| undefinedL904
isNotNullishnullundefinedL935
isNever恒为falseL958
isObjectnull、非数组的objectL1042
hasProperty对象上存在某属性L1140
isTagged_tag字段严格等于给定值L1175
isErrorinstanceof ErrorL1208
isUint8Arrayinstanceof Uint8ArrayL1238
isDateinstanceof DateL1267
isIterable具有Symbol.iteratorL1297
isPromiseinstanceof PromiseL1326
isPromiseLike可 then 化对象L1356
isRegExpinstanceof RegExpL1385
isSet/isMapinstanceof Set/instanceof MapL490 / L522
isTruthy!!inputL458
isTupleOf(n)数组长度恰好为nL393
isTupleOfAtLeast(n)数组长度至少为nL426

3.1 面向"标签联合"的两个关键守卫

  • hasProperty(property):基于isObjectKeyword(self) && (property in self)实现(L1140-L1147),返回Refinement<unknown, { [K in P]: unknown }>,用于确认某个属性存在;
  • isTagged(tag):内部调用hasProperty(self, "_tag") && self["_tag"] === tag(L1175-L1181),用于判别带_tag字段的可辨识联合:
import { Predicate } from "effect" const isOk = Predicate.isTagged("Ok") isOk({ _tag: "Ok", value: 1 }) // => true isOk({ _tag: "Err", error: "boom" }) // => false

这类守卫在解析 Effect 的EitherOptionExit等数据类型时非常常用。


四、组合 API:and/or/not/compose

原文档明确指出谓词可以通过Predicate.andPredicate.orPredicate.notPredicate.compose进行组合。这四个 API 的实现都集中在 Predicate.ts 的组合子(combinators)部分,且全部支持柯里化(curried)与管道(pipe)两种调用方式

4.1and:全部满足才为真

export const and: { /* ... 多个重载 ... */ } = dual( 2, <A>(self: Predicate<A>, that: Predicate<A>): Predicate<A> => (a) => self(a) && that(a) )

源码见 L1632-L1637。要点:

  • 语义为逻辑与,self(a) && that(a)在第一个返回false的谓词处短路
  • 当两个参数都是Refinement时,返回类型会收窄为两者的交集B & C
  • 测试覆盖见 Predicate.test.ts:
const isPositive: Predicate.Predicate<number> = (n) => n > 0 const isLessThan2: Predicate.Predicate<number> = (n) => n < 2 const p = pipe(isPositive, Predicate.and(isLessThan2)) p(1) // => true p(-1) // => false(不满足 isPositive) p(3) // => false(不满足 isLessThan2)

4.2or:任一满足即为真

export const or = dual( 2, <A>(self: Predicate<A>, that: Predicate<A>): Predicate<A> => (a) => self(a) || that(a) )

源码见 L1586-L1591。要点:

  • 语义为逻辑或,self(a) || that(a)在第一个返回true的谓词处短路
  • 当两个参数都是Refinement时,返回类型收窄为两者的并集B | C
  • 测试见 Predicate.test.ts:
const p = pipe(isPositive, Predicate.or(isNegative)) p(-1) // => true p(1) // => true p(0) // => false

4.3not:取反

export function not<A>(self: Predicate<A>): Predicate<A> { return (a) => !self(a) }

源码见 L1554-L1556。实现就是简单的布尔翻转,测试见 Predicate.test.ts:

const isNotString = Predicate.not(Predicate.isString) isNotString(1) // => true

4.4compose:串联两个 Refinement,逐级收窄

export const compose = dual( 2, <A, B extends A, C extends B>(ab: Refinement<A, B>, bc: Refinement<B, C>): Refinement<A, C> => (a): a is C => ab(a) && bc(a) )

源码见 L1420-L1429。这是四个 API 中类型能力最强的一个:它把Refinement<A, B>Refinement<B, C>(或作用于B的普通Predicate)串联成Refinement<A, C>——第一次收窄的输出恰好是第二次检查的输入,最终完成从AC的两级收窄。测试见 Predicate.test.ts:

const isString: Predicate.Refinement<unknown, string> = (u): u is string => typeof u === "string" const isNonEmptyString: Predicate.Refinement<string, NonEmptyString> = (s): s is NonEmptyString => s.length > 0 const refinement = pipe(isString, Predicate.compose(isNonEmptyString)) refinement("a") // => true refinement("") // => false refinement(null) // => false

composeand的区别值得注意:and适用于同一输入A上的多条件叠加;compose则用于类型逐级收窄的链式流程(如"先确认是 string,再确认非空")。二者一横一纵,覆盖了谓词组合的两个维度。


五、结构化组合:Struct/Tuple/mapInput

除布尔组合子外,Predicate还提供把多个谓词"提升"到结构化数据上的工具,这些在原文档没有展开,但对实战极其重要。

5.1Predicate.Struct:按字段名逐字段校验对象

源码见 L1508-L1525,实现要点:

  • 遍历Object.keys(fields),对每个键依次应用对应谓词,遇到第一个失败即返回false(短路)
  • 只检查指定的键,忽略额外的键
  • 任一字段谓词是Refinement时,整体返回Refinement并对字段做类型收窄。
import { Predicate } from "effect" const userCheck = Predicate.Struct({ id: Predicate.isNumber, name: Predicate.isString }) userCheck({ id: 1, name: "Ada" }) // => true

测试中的短路验证(Predicate.test.ts):当第一个字段谓词返回false时,第二个谓词根本不会被调用(calls === 1),并且{ a: 1, b: "ok", extra: true }这类带额外键的对象同样可以通过。

5.2Predicate.Tuple:按位置逐元素校验元组

源码见 L1459-L1475,与Struct对称,逐个下标应用谓词、同样短路:

const tupleCheck = Predicate.Tuple([(n: number) => n > 0, Predicate.isString]) tupleCheck([1, "ok"]) // => true tupleCheck([-1, "ok"]) // => false

测试见 Predicate.test.ts。

5.3mapInput:先映射、再判断

源码见 L360-L363,mapInput(self, f)返回一个新谓词,等价于(b) => self(f(b))——先用fB投影为A,再应用原谓词。典型用途是"检查字符串长度"这类需求:

const isLongerThan2 = Predicate.mapInput((s: string) => s.length)((n: number) => n > 2) isLongerThan2("hello") // => true

测试见 Predicate.test.ts,同时验证了柯里化与管道两种调用方式。


六、实战:从unknown到安全访问(基于官方示例展开)

教程配套代码 10_predicate/01_basics.ts 展示了最基础的用法——对unknown值逐层守卫:

/** * @title Using the Predicate module */ import { Predicate } from "effect" const thing: unknown = { a: 1 } if (Predicate.isObject(thing)) { if (Predicate.isNumber(thing.a)) { console.log("number", thing.a) } }

把它升级为真实场景(解析不可信的 JSON 数据并安全读取嵌套字段),结合本文前面所有知识点:

import { Predicate, pipe } from "effect" // 1. 把 JSON.parse 的结果先认定为 unknown,强制显式校验 const raw: unknown = JSON.parse(`{"user":{"id":1,"name":"Ada","tags":["ts"]}}`) // 2. 用组合子描述"形状" const hasUser = Predicate.hasProperty("user") const userIsObject = Predicate.compose(hasUser, Predicate.isObject) const isExpectedUser = pipe( userIsObject, Predicate.and(Predicate.Struct({ id: Predicate.isNumber, name: Predicate.isString })) ) // 3. 守卫通过后,类型自动收窄,可以安全访问 if (isExpectedUser(raw)) { console.log(raw.user.id, raw.user.name) // 类型安全 }

同样的模式可以套在Either结果、外部 API 响应、环境变量解析等一切"运行时才知道真实形状"的数据上。从源码结构看,这正是 Effect 生态中众多模块(如SchemaConfighttp客户端)底层校验的基础设施之一,教程文档也在 ai-docs/src/index.md 中强调:查找 Effect 相关知识时应以本仓库文档与源码为准。


七、组合子的完整能力矩阵

除了and/or/not/compose四个文档点名的组合子,模块还提供更多逻辑组合,全部有测试覆盖(Predicate.test.ts):

API语义短路行为测试位置
and逻辑与,收窄为交集首个false即停L136
or逻辑或,收窄为并集首个true即停L126
not逻辑非L116
compose两级 Refinement 串联收窄任一级失败即停L18
xor恰好一个为真L146
eqv两个谓词结果一致L155
implies蕴含关系L164
nor/nand或非 / 与非L173 / L182
every/some数组全满足 / 任一满足短路L191 / L207

例如xor(L1667-L1670)实现为self(a) !== that(a),即两个谓词结果不同才为真。every/some则把谓词提升到数组层面,直接配合Array.prototype风格的语义使用。


八、实践建议与注意事项

  1. unknown出发,显式收窄JSON.parsefetch响应、process.env取值的类型都应先落为unknown,再用Predicate守卫收窄,避免"类型断言掩盖运行时错误";
  2. 优先复用内置守卫:牢记原文档的"NEVER"规范——检查对象用isObject、检查字符串用isString、检查可辨识联合用isTagged,不要重复造轮子;
  3. 组合优于嵌套:多层if可以重构为Predicate.and/Struct/compose的组合表达式,让校验逻辑成为可命名、可复用、可单测的独立值;
  4. 利用类型收窄:组合时优先选用带Refinement签名的守卫(内置守卫基本都满足),这样and得到交集、or得到并集、compose得到逐级收窄,编译器会替你把关后续访问;
  5. 注意短路语义andStructTuple都在首个失败处短路,or在首个成功处短路。若你的谓词有副作用(不建议),务必理解求值顺序;
  6. 关注测试与文档:完整的组合子测试在 packages/effect/test/Predicate.test.ts,源码内每个 API 都带有可运行的 JSDoc 示例,是学习与排查行为的最佳参考。

小结

Effect 的Predicate模块把"运行时类型守卫"从散落的typeof检查提升为一套可复用、可组合、类型安全的标准设施:内置数十个现成守卫覆盖 JS 常见值与标签联合,and/or/not/compose提供布尔级组合,Struct/Tuple/mapInput提供结构级提升。遵循原文档"不要自写isRecord/isString"的规范,配合本文的实战模式,你可以在任何需要处理unknown数据的代码路径上写出比手写if链更可靠、更易维护的校验逻辑。

【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询