
Effect 中Array.groupBy/Iterable.groupBy返回类型修复有限字符串与唯一 Symbol 键联合的精确保留【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect导读本篇文章讲解 Effect 4.0 中一项针对分组 API 的类型系统修复Array.groupBy与Iterable.groupBy的返回类型现在会保留有限字符串字面量与唯一 Symbol 的键联合而非将它们拓宽为string/symbol。这一改动直接改善了键的自动补全体验并阻止访问选择器永远不可能产生的键。文章将结合仓库中的实现源码、类型测试与运行测试说明修复前的问题、修复后的新返回类型Record.ReadonlyRecord.GroupByResult、它的类型逻辑与典型使用模式。变更背景为什么分组结果的键类型需要精确Effect 中Array.groupBy与Iterable.groupBy用于将可迭代元素按照一个返回string或symbol的键函数分桶每个键对应一个NonEmptyArray。在实际业务里选择器往往返回的是有限键集合例如type GroupName A | B | C const byGroup Array.groupBy(people, (person) person.group as GroupName)在修复之前分组结果的键会被拓宽为string或symbol。这意味着丢失已知键的自动补全编辑器无法提示byGroup.A、byGroup.B等已知键类型层面丢失了选择器能产生的键集合信息允许访问不存在的键由于结果是Recordstring, NonEmptyArray...访问byGroup.Z在类型上完全合法但运行时该键的组必然缺失值为undefined形成“类型声称存在、运行时必然缺席”的隐患。这次变更通过新的返回类型Record.ReadonlyRecord.GroupByResult同时解决这两个问题保留有限键并且因为任何一组在运行时都可能缺席将这些键的属性标记为可选而开放式的string/symbol选择器则保留原有的记录索引签名行为不变。新返回类型Record.ReadonlyRecord.GroupByResult修复的核心是一个全新的工具类型定义在 packages/effect/src/Record.tsexport type GroupByResultK extends string | symbol, V [NonLiteralKeyK] extends [K] ? RecordK, V : PartialRecordK, V其逻辑是若K是非字面量键即经过NonLiteralKey归一化后仍然等于K说明选择器是开放式的string/symbol返回RecordK, V保留索引签名否则K是有限键联合字符串字面量或唯一 Symbol返回PartialRecordK, V键被保留且属性可选。该类型从 4.0 版本开始提供见 Record.ts 中since 4.0.0标注并在 Array.ts 与 Iterable.ts 的groupBy签名中被使用例如export const groupBy: { A, K extends string | symbol( f: (a: A) K ): (self: IterableA) Record.ReadonlyRecord.GroupByResultK, NonEmptyArrayA // ... }NonLiteralKey区分有限键与开放键GroupByResult的判断依赖ReadonlyRecord.NonLiteralKey类型定义于 Record.tsexport type NonLiteralKeyK extends string | symbol K extends string ? IsFiniteStringK extends true ? string : K : symbol它把有限字符串字面量转换为string、把 Symbol 键归一化为symbol同时保留string之类的开放类型本身。其背后还有IsFiniteString这一逐字符递归判断类型Record.ts用于判定一个字符串类型是否是有限的字面量联合如A | B还是包含string、模板字符串等无限集合。修复前后对比类型层面能观察到什么以有限键为例。修复前Array.groupBy返回Recordstring, NonEmptyArray...修复后返回PartialRecordA | B, NonEmptyArray...。这一差异在类型测试中有明确验证见 packages/effect/typetest/Array.tst.ts 与 packages/effect/typetest/Iterable.tst.tsconst bySingleKey Array.groupBy([1, 2, 3], () key as const) // 类型PartialRecordkey, NonEmptyArraynumber bySingleKey.key // 类型NonEmptyArraynumber | undefined // ts-expect-error 属性 other 不存在 void bySingleKey.other可见bySingleKey.key的类型是NonEmptyArraynumber | undefined——键被保留可自动补全同时因组可能缺席而为可选访问bySingleKey.other直接报类型错误——选择器不可能产生的键被拒绝访问。对于有限键联合与唯一 Symbol 的组合同样被精确保留const symA Symbol.for(a) const symB Symbol.for(b) const symC Symbol.for(c) Array.groupBy([a, b], (s) s a ? symA : s b ? symB : symC) // 类型PartialRecordtypeof symA | typeof symB | typeof symC, NonEmptyArraystring而开放式选择器保持不变Array.groupBy([1, 2, 3], (n) String(n)) // 类型Recordstring, NonEmptyArraynumber保留索引签名 Array.groupBy([a, b], Symbol.for) // 类型Recordsymbol, NonEmptyArraystring对应运行时行为可参考 packages/effect/test/Array.test.ts 与 packages/effect/test/Iterable.test.ts 中的测试空输入返回空对象、不同键归入不同桶、Symbol 键按引用正确分组等。实际用法示例下面结合 Iterable.ts 文档中的示例展示新返回类型在日常代码中的表现import { Iterable } from effect // 按字符串长度分组开放式 string 键保留索引签名 const words [a, bb, ccc, dd, eee, f] const byLength Iterable.groupBy(words, (word) word.length.toString()) // byLength { 1: [a, f], 2: [bb, dd], 3: [ccc, eee] } // 按首字母分组 const names [Alice, Bob, Charlie, David, Anna, Betty] const byFirstLetter Iterable.groupBy(names, (name) name[0]) // byFirstLetter { A: [Alice, Anna], B: [Bob, Betty], C: [Charlie], D: [David] } // 按类别分组有限键联合 const items [ { name: apple, category: fruit as const }, { name: carrot, category: vegetable as const }, { name: banana, category: fruit as const }, { name: broccoli, category: vegetable as const } ] const byCategory Iterable.groupBy(items, (item) item.category) // 类型PartialRecordfruit | vegetable, NonEmptyArray...Array.groupBy的用法完全一致并且两者都同时提供柯里化与非柯里化两种调用形式数据优先 / 数据在后的pipe风格可参考 Array.ts 中的示例与 Array.tst.ts 中对两种调用形式返回类型一致性的验证。实现细节底层分组逻辑groupBy的运行时实现位于 Array.ts 与 Iterable.ts二者实现一致核心是const out: Recordstring | symbol, NonEmptyArrayA {} for (const a of self) { const k f(a) if (Object.hasOwn(out, k)) { out[k].push(a) } else { InternalRecord.assignProperty(out, k, [a]) } } return out两点值得注意用Object.hasOwn判断键是否已存在避免原型链上的键干扰分组键的写入没有直接使用out[k] ...而是调用内部工具assignProperty见 packages/effect/src/internal/record.ts它对__proto__这类特殊键使用Object.defineProperty处理避免原型污染问题。也就是说这次变更只影响类型层面运行时分组行为桶的生成、NonEmptyArray语义、顺序保持未变变化的只是暴露给用户的返回类型描述。如何获取该修复该修复由 PR 引入见 packages/effect/CHANGELOG.md属于effect包的 patch 级别变更。当前仓库中packages/effect/package.json的版本为4.0.0-rc.115见 packages/effect/package.json。使用方式不变npm install effect # 或 pnpm add effect在包含该修复的版本上Array.groupBy与Iterable.groupBy的返回类型即会呈现为Record.ReadonlyRecord.GroupByResultK, NonEmptyArrayA。总结变更内容Array.groupBy与Iterable.groupBy的返回类型从“键一律拓宽为string/symbol”改为由Record.ReadonlyRecord.GroupByResult描述有限字符串字面量与唯一 Symbol 键联合被精确保留且属性可选。解决的问题恢复已知键的自动补全并在类型层面禁止访问选择器不可能产生的键开放式string/symbol选择器行为保持不变。实现位置返回类型定义于 Record.ts分组逻辑位于 Array.ts 与 Iterable.ts类型与运行时行为分别由 Array.tst.ts、Iterable.tst.ts 与 Array.test.ts、Iterable.test.ts 保障。这是一次典型的“类型先行”改进运行时无需改动仅靠精确的类型描述就同时提升了开发体验补全与类型安全杜绝越界键访问。【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考