TypeScript中如何根据Record值类型生成对应自定义键的输出Record
问题描述
现有convert函数,入参为对象,对象每个属性值支持两种类型:
- 普通函数
- 包含
pending/fulfilled/rejected三个函数属性的对象
原入参类型定义存在语法错误(rejected: {} => any写法有误,无参函数正确写法为() => any),修正后基础类型如下:
type Input = { [K: string]: (() => any) | { pending: () => any, fulfilled: () => any, rejected: () => any } }
需要实现返回值类型的动态推导,规则为:
- 入参属性值为普通函数时,返回值保留原属性名,值为原函数
- 入参属性值为三状态对象时,返回值生成
${原属性名}Pending、${原属性名}Fulfilled、${原属性名}Rejected三个属性,值分别对应原对象里的三个方法
预期类型效果示例:
const result = convert({ sync() {}, async: { pending() {}, fulfilled() {}, rejected() {} } }); // 以下调用均需通过类型校验 result.sync(); result.asyncPending(); result.asyncFulfilled(); result.asyncRejected();
现有仅支持固定后缀拼接的写法无法满足分支判断、一对多生成键的需求:
type Result<Input> = { [key in keyof Input as `${string & key}Fulfilled`]: any }
实现方案
核心通过分布式条件类型、模板字面量类型、联合转交叉三个TypeScript类型特性实现,完整代码如下:
// 定义入参合法值类型 type InputValue = (() => any) | { pending: () => any fulfilled: () => any rejected: () => any } type ConvertInput = Record<string, InputValue> // 单个入参键的转换逻辑:根据值类型生成对应的键值对对象 type ProcessSingleKey<K extends PropertyKey, V> = V extends () => any // 值为函数时,保留原键 ? Record<K, V> // 值为三状态对象时,生成三个带后缀的键,对应各自的方法 : V extends { pending: infer P, fulfilled: infer F, rejected: infer R } ? K extends string ? Record<`${K}Pending`, P> & Record<`${K}Fulfilled`, F> & Record<`${K}Rejected`, R> : never : never // 工具类型:联合类型转交叉类型 type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never // 最终返回值类型 type ConvertResult<T extends ConvertInput> = UnionToIntersection<{ [K in keyof T]: ProcessSingleKey<K, T[K]> }[keyof T]> // convert函数类型定义 declare function convert<T extends ConvertInput>(input: T): ConvertResult<T>
效果验证
用示例代码测试可得到符合预期的类型推导,甚至能保留每个方法的参数、返回值类型:
const result = convert({ sync() { return 'sync res' }, async: { pending() { return 'pending res' }, fulfilled(res: number) { return res }, rejected(err: Error) { return err.message } } }) // 类型推导完全正确 result.sync() // 推导类型为 () => string result.asyncPending() // 推导类型为 () => string result.asyncFulfilled(123) // 推导类型为 (res: number) => number result.asyncRejected(new Error('test')) // 推导类型为 (err: Error) => string
实现原理
- 普通映射类型仅支持一个输入键映射为一个输出键,因此先将每个输入键单独转换为独立的对象类型:值为函数时生成单键对象,值为三状态对象时生成三键交叉对象
- 通过索引访问
[keyof T]拿到所有单属性转换结果的联合类型 - 用
UnionToIntersection工具将联合类型合并为单个对象类型,得到最终的返回结构
内容的提问来源于stack exchange,提问作者Jan Gao
相关产品推荐
相关产品推荐

