TypeScript柯里化函数参数类型推断失效问题求解
toOption函数的类型推断问题 问题背景
我有一个TypeScript函数toOption,作用是将对象转换为选择项,其类型系统保证三个约束:
ValueKey必须是ObjectIn的属性键- 如果
LabelKey是字符串,必须是ObjectIn的属性键 - 如果
LabelKey是函数,其参数能正确推断为ObjectIn
将该函数柯里化(拆分最后一个参数)后出现类型推断失效:
- 若所有泛型参数放在外层函数,
ObjectIn无法正确推断 - 若将
ObjectIn移到内层函数,会因提前引用泛型参数导致语法报错
需求:保持柯里化的同时保留原类型约束,接受LabelKey为函数时需显式指定ObjectIn,但LabelKey为字符串时仍保留自动推断能力。
方案一:利用函数重载区分LabelKey类型
通过重载签名拆分两种场景,让TypeScript在不同分支下完成正确的类型推断,同时保留所有约束。
// 重载1:LabelKey为字符串,支持自动推断ObjectIn function toOption<ValueKey extends string, LabelKey extends string>( valueKey: ValueKey, labelKey: LabelKey ): <ObjectIn extends Record<ValueKey, unknown> & Record<LabelKey, unknown>>( obj: ObjectIn ) => { value: ObjectIn[ValueKey]; label: ObjectIn[LabelKey] }; // 重载2:LabelKey为函数,需显式指定ObjectIn function toOption<ValueKey extends string, ObjectIn extends Record<ValueKey, unknown>>( valueKey: ValueKey, labelKey: (obj: ObjectIn) => string ): (obj: ObjectIn) => { value: ObjectIn[ValueKey]; label: string }; // 核心实现 function toOption(valueKey: string, labelKey: string | ((obj: any) => string)) { return (obj: any) => { const value = obj[valueKey]; const label = typeof labelKey === 'function' ? labelKey(obj) : obj[labelKey]; return { value, label }; }; } // 测试字符串LabelKey:自动推断ObjectIn const userToOption = toOption('id', 'name'); const option1 = userToOption({ id: 1, name: 'Alice' }); // 类型正确:{ value: number; label: string } // 测试函数LabelKey:显式指定ObjectIn type User = { id: number; name: string; age: number }; const userToOptionWithFn = toOption<'id', User>('id', (user) => `${user.name} (${user.age})`); const option2 = userToOptionWithFn({ id: 2, name: 'Bob', age: 30 }); // 类型正确:{ value: number; label: string }
这种方案通过重载将两种场景分离:字符串场景把ObjectIn放在内层函数,让TypeScript通过传入的对象自动推断;函数场景要求显式指定ObjectIn,避免推断失效,同时完全保留三个类型约束。
方案二:条件类型+泛型顺序调整
通过调整泛型参数顺序,结合条件类型统一处理Label的类型,让内层函数的ObjectIn能被正确推断,同时满足约束要求。
// 定义Label类型的条件判断逻辑 type LabelType<LabelKey, ObjectIn> = LabelKey extends keyof ObjectIn ? ObjectIn[LabelKey] : LabelKey extends (obj: ObjectIn) => infer R ? R : never; function toOption<ValueKey extends string, LabelKey extends string | ((obj: any) => any)>( valueKey: ValueKey, labelKey: LabelKey ) { return <ObjectIn extends Record<ValueKey, unknown> & (LabelKey extends string ? Record<LabelKey, unknown> : unknown)>( obj: ObjectIn ): { value: ObjectIn[ValueKey]; label: LabelType<LabelKey, ObjectIn> } => { const value = obj[valueKey]; const label = typeof labelKey === 'function' ? (labelKey as (obj: ObjectIn) => any)(obj) : obj[labelKey as keyof ObjectIn]; return { value, label }; }; } // 测试字符串LabelKey:自动推断 const productToOption = toOption('sku', 'productName'); const productOption = productToOption({ sku: 'ABC123', productName: 'Laptop' }); // 类型推断正确 // 测试函数LabelKey:显式指定泛型 type Product = { sku: string; productName: string; price: number }; const productToOptionWithFn = toOption<'sku', (obj: Product) => string>('sku', (p) => `${p.productName} - $${p.price}`); const productOption2 = productToOptionWithFn({ sku: 'XYZ789', productName: 'Phone', price: 999 }); // 类型正确
该方案用条件类型统一处理Label的输出类型,外层声明ValueKey和LabelKey,内层通过交叉类型约束ObjectIn与前两者的关系,既保证柯里化结构,又满足所有类型约束。函数类型的LabelKey需要显式指定泛型来绑定ObjectIn,字符串场景则自动完成推断。
方案三:利用泛型延迟推断(TypeScript 4.7+)
依赖TypeScript 4.7及以上版本的泛型延迟推断特性,将ObjectIn放在内层函数,通过交叉类型避免提前引用报错,同时保留约束。
function toOption<ValueKey extends string, LabelKey extends string | ((obj: any) => any)>( valueKey: ValueKey, labelKey: LabelKey ) { return <ObjectIn extends Record<ValueKey, unknown> & (LabelKey extends keyof ObjectIn ? Record<LabelKey, unknown> : {})>( obj: ObjectIn ) => { const value = obj[valueKey]; const label = typeof labelKey === 'function' ? (labelKey as (obj: ObjectIn) => string)(obj) : obj[labelKey as keyof ObjectIn]; return { value, label } as const; }; } // 字符串LabelKey场景:自动推断ObjectIn const bookToOption = toOption('isbn', 'title'); const bookOption = bookToOption({ isbn: '978-1234567890', title: 'TypeScript Guide' }); // 函数LabelKey场景:显式指定ObjectIn type Book = { isbn: string; title: string; author: string }; const bookToOptionWithFn = toOption<'isbn', (obj: Book) => string>('isbn', (b) => `${b.title} by ${b.author}`); const bookOption2 = bookToOptionWithFn({ isbn: '978-0987654321', title: 'Advanced TS', author: 'Jane Doe' });
这个方案利用TypeScript对泛型的延迟推断能力,内层函数的ObjectIn会根据传入的对象自动推断,同时通过交叉类型保证ValueKey和字符串类型的LabelKey都属于ObjectIn的属性,函数类型的LabelKey则需要显式指定泛型来绑定ObjectIn类型,完全满足需求。
内容的提问来源于stack exchange,提问作者Michal Kurz

