TypeScript 实现嵌套属性数组按首元素属性推导对应reader函数的类型校验
问题根因
- 原有类型定义的核心缺陷是泛型分发后的联合数组丢失了属性与reader函数的绑定关系:当
K extends keyof T是keyof T联合类型时,泛型分发生成的元组类型为["name", reader<string>] | ["age", reader<number>] | ["sex", reader<HeroSex>],对应的数组类型是这个联合类型的数组。TS在做类型检查时,会基于数组协变、函数参数逆变的规则允许跨分支兼容:比如["sex", readNumber]可以兼容["sex", reader<HeroSex>]分支(因为数字枚举HeroSex本质是number的子类型,reader<number>根据逆变规则是reader<HeroSex>的合法子类型),最终导致类型校验失效。 - 原有写法没有捕捉传入数组的具体元组结构,TS无法对每个数组元素做单独的键与reader的绑定校验,只能在宽松的联合数组范围内做兼容判断。
修复方案
- 调整类型定义,实现逐元素的精确校验,强制每个数组元素的属性名、对应属性值类型与reader函数严格匹配
- 保持原有的函数签名逻辑与调用形式不变,仅优化类型层的校验规则
修复后的完整代码:
enum HeroSex { male, female, unknown, } interface Hero { name: string; age: number; sex: HeroSex; } type reader<T> = (value: T) => void; // 新增:精确类型相等检查工具,避免函数逆变导致的宽松兼容 type ExactEqual<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false; // 调整readProperties的泛型签名,自动捕捉传入的reader数组结构并逐元素校验 const readProperties = < T extends object, R extends Array<[keyof T, (value: any) => void]> >( Obj: T, readers: R extends Array<infer Elem> ? Elem extends [infer PropKey, infer ReaderFn] ? PropKey extends keyof T ? ExactEqual<ReaderFn, reader<T[PropKey]>> extends true ? Elem : [PropKey, reader<T[PropKey]>] : never : never : never[] ) => { for (const [prop, reader] of readers) { reader(Obj[prop as keyof T]); } }; const readString: reader<string> = (value) => { console.log(`the string is ${value}`); }; const readNumber: reader<number> = (value) => { console.log(`the number is ${value.toFixed()}`); }; const readHeroSex: reader<HeroSex> = (value) => { console.log(`the enum is ${value.toString()}`); }; const realHero: Hero = { name: "batman", age: 38, sex: HeroSex.male, };
校验效果
- 属性名支持自动补全:输入数组第一个元素时,TS会提示
name/age/sex三个合法选项 - 严格的类型匹配:传入
["sex", readNumber]时,TS会抛出类型错误,提示该位置需要reader<HeroSex>类型的函数,不接受仅接收number的reader - 合法调用无报错:传入
["sex", readHeroSex]、["age", readNumber]、["name", readString]的组合时,可正常通过类型校验 - 原有API调用逻辑完全不变,无需修改业务代码
内容的提问来源于stack exchange,提问作者Cosimo Chellini
相关产品推荐
相关产品推荐

