TypeScript泛型映射类型下自动补全与参数推断无法同时生效
TypeScript嵌套对象类型:同时实现自动补全与函数签名推断
问题描述
在类型实现中,调用personTypeX({...})时需要同时实现自动补全与函数签名推断两项能力,但现有两个实现版本仅能满足单项目标,无法二者兼得。
代码可在Playground中查看。
基础类型约束规则
- 若入参Person对象包含
firstName属性,其值必须为string类型 - 若入参Person对象包含
age属性,其值必须为number类型 - 其余固定属性遵循相同的类型匹配规则
- 若入参Person对象包含
firstName、lastName、age、favouriteFood、getLegCount范围外的属性,该属性的值必须本身也是符合Person类型约束的对象
全局要求
- 必须能检测入参任意层级的非法值,在对应位置精准标记类型错误:现有
personType1()与personType2()均已实现该能力 - 在
personTypeX({})内输入时,必须提供firstName至getLegCount范围内所有固定键的自动补全提示:现有仅personType2({})实现了该能力 - 若键(本例中仅
getLegCount)约束值为函数类型,必须正确推断该函数的签名:现有仅personType1({})实现了该能力
现有问题代码
type FixedType = { firstName: string, lastName: string, age: number, favouriteFood: string[], getLegCount: (numHeads: number) => number, }; type PersonType<T> = { [K in (keyof T | keyof FixedType)]?: K extends keyof FixedType ? FixedType[K] : K extends keyof T ? T[K] extends object ? PersonType<T[K]> : PersonType<{}> : never } // ---- ---- ---- ---- ---- // 版本1:可正确推断函数签名,无自动补全 function personType1<T extends PersonType<T>>(t: T) { return t; } const person11 = personType1({ firstName: 'Mark', lastName: 'Antony', holger: { age: 12, paul: { // 输入'a'尝试自动补全'age' => 不生效 favouriteFood: ['cheese'], firstName: 'Paul', getLegCount: (numHeads) => 2, // 悬浮查看'numHeads'类型 => 正确推断为number => 生效 } }, }); // ---- // 版本2:有自动补全,函数参数被推断为any function personType2<T extends PersonType<T>>(t: T & PersonType<T>) { return t; } const person1 = personType2({ firstName: 'Mark', lastName: 'Antony', holger: { age: 12, paul: { // 输入'a'尝试自动补全'age' => 生效 favouriteFood: ['cheese'], firstName: 'Paul', getLegCount: (numHeads) => 2, // 悬浮查看'numHeads'类型 => 推断为any => 不生效 // 原因是交叉类型后该属性签名为 ((numHeads: any) => number) & ((numHeads: number) => number) } }, });
问题原因
两个版本失效的核心原因:
- 版本1仅通过泛型约束校验类型,没有给对象字面量提供提前的上下文类型,TS在推断时无法感知到可选的固定属性,因此不会弹出自动补全提示
- 版本2将参数写为
T & PersonType<T>,在函数类型的逆变位置产生交叉类型,TS会将交叉函数的参数类型推断为any,导致签名推断失效
实现方案
将类型拆分为「校验用泛型约束」和「自动补全用上下文基础类型」两部分,既给对象字面量提供固定属性的上下文提示,又不会产生破坏函数签名推断的交叉类型:
type FixedType = { firstName: string, lastName: string, age: number, favouriteFood: string[], getLegCount: (numHeads: number) => number, }; // 泛型约束:递归校验所有层级的属性类型是否合规 type PersonConstraint<T> = { [K in keyof T]: K extends keyof FixedType ? FixedType[K] : T[K] extends object ? PersonConstraint<T[K]> : never }; // 上下文类型:提供固定属性自动补全,同时约束自定义属性为合法的Person结构 type PersonShape = { [K in keyof FixedType]?: FixedType[K] } & { [key: string]: PersonShape } // 最终实现 function personTypeX<T extends PersonConstraint<T>>(t: T & PersonShape): T { return t; }
效果验证
- 任意层级输入属性时,会自动弹出
firstName/age等所有固定键的补全提示 - 书写
getLegCount方法时,参数numHeads会被自动推断为number类型,无需手动标注 - 任意层级的类型错误(如给
age传字符串、自定义属性传原始类型值)都会在对应位置精准报红
内容的提问来源于stack exchange,提问作者Page not found
相关产品推荐
相关产品推荐

