TypeScript中带类型安全遍历映射类型Record生成对应实例的最佳方案
问题场景与复现
我有两个Record(选项Record和实例Record),它们的键与值存在对应关系,通过带泛型值的映射类型保证该对应性。任务是基于选项Record生成实例Record:普通for..in循环会将对象键类型降级为string并丢失键值对应,采用Array.forEach泛型回调仍无法正确设置类型。
问题复现代码:
type key = 'key1' | 'key2'; type someOption<T extends key> = `${T}`; type someInstance<T extends key> = Array<`${T}`>; type record1 = { [T in key]: someOption<T>; }; type record2 = { [T in key]: someInstance<T>; } const rec1: record1 = { key1: 'key1', key2: 'key2', }; const rec2: Partial<record2> = {}; Object.entries(rec1).forEach(<T extends key>([recKey, recValue]: [T, someOption<T>]) => { rec2[recKey] = [recValue, recValue]; // 此处出现类型错误 }); // 遍历完成后移除Partial约束 // return rec2 as record2;
错误信息
第18行TypeScript错误:
TS2322: Type
${T}[] is not assignable to type Partial[T]
Type${T}[] is not assignable to type
someInstance<'key1'> & someInstance<'key2'>
Type${T}[] is not assignable to type someInstance<'key1'>
Type${T}is not assignable to type 'key1'
Type key is not assignable to type 'key1'
Type 'key2' is not assignable to type 'key1'
疑问点
解析后rec2[recKey]的类型变为Array<key1> & Array<key2>(交叉类型),而非预期的联合类型,不清楚为何会出现这种情况,也不确定当前实现是否有误。使用TypeScript 5.0.4和Node.js 16.19.1。
问题原因
TypeScript在处理索引类型为联合类型的访问时,默认会将结果推断为交叉类型而非联合类型。这是因为TypeScript无法在forEach的泛型回调中跟踪每个循环迭代的具体键类型,只能将recKey视为整个联合类型key,进而导致rec2[recKey]被推断为所有可能值类型的交叉(someInstance<'key1'> & someInstance<'key2'>),而交叉的数组类型实际上不可赋值(Array<'key1'>和Array<'key2'>无交集),因此抛出类型错误。
解决方案
方案1:使用类型断言(简单直接)
在赋值时对rec2[recKey]进行类型断言,明确告知TypeScript当前赋值的类型与键对应:
Object.entries(rec1).forEach(<T extends key>([recKey, recValue]: [T, someOption<T>]) => { rec2[recKey] = [recValue, recValue] as someInstance<T>; });
该方式快速解决问题,适合简单场景,缺点是需要手动维护断言的准确性。
方案2:使用reduce代替forEach利用类型推断
reduce可通过返回值的类型推断更精准地维护键值对应关系,直接生成目标类型:
const rec2 = Object.entries(rec1).reduce((acc, [recKey, recValue]) => { acc[recKey as key] = [recValue, recValue] as someInstance<typeof recKey>; return acc; }, {} as Partial<record2>) as record2;
方案3:手动遍历每个键(类型最安全)
如果键的数量较少,直接枚举每个键可完全避免类型推断问题:
const rec2: record2 = { key1: [rec1.key1, rec1.key1], key2: [rec1.key2, rec1.key2], };
这种方式类型最安全,无任何类型断言,适合键集合固定且数量不多的场景。
方案4:封装类型安全的遍历工具函数
封装泛型工具函数,确保遍历过程中键值类型的对应性,提升复用性:
function mapRecord<K extends string | number | symbol, V, R>( record: Record<K, V>, mapper: (key: K, value: V) => R ): Record<K, R> { const result = {} as Record<K, R>; for (const key in record) { if (Object.prototype.hasOwnProperty.call(record, key)) { result[key] = mapper(key as K, record[key]); } } return result; } const rec2 = mapRecord(rec1, (key, value) => [value, value] as someInstance<typeof key>);
内容的提问来源于stack exchange,提问作者Bunny Boss

