TypeScript显式指定首个泛型时如何推断第二个泛型的键类型?
问题现象
泛型函数useTest初始定义包含两个泛型参数:
TElement:约束为继承Element,默认值为ElementTObj:约束为继承{},默认值为{}
函数接收一个对象类型入参,返回const断言的二元组:
- 第一个元素是遍历入参所有键生成的、值固定为
true的Record<keyof TObj, true>类型映射 - 第二个元素为
TElement类型实例
实际调用时存在类型推断异常:
- 不显式指定泛型、直接传入示例对象
obj调用时,TS可正确推断返回的keys类型为Record<"one" | "two", true> - 显式指定第一个泛型为
HTMLDivElement调用时,第二个泛型会回退到默认值{},导致keys类型被错误推断为Record<never, true>
目标是实现:显式指定第一个泛型参数时,编译器仍可从传入的入参自动推断第二个泛型TObj的键类型,无需手动书写useTest<HTMLDivElement, typeof obj>(obj)这类冗余泛型指定代码。
问题根因
TypeScript 目前不支持泛型参数的「部分显式指定、部分自动推断」能力:当调用时显式传入泛型列表中靠前的参数后,后续未显式传入的参数会直接回退到定义的默认值,不会再从函数入参触发类型推断。
实现方案
通过泛型柯里化拆分解决:把需要用户手动指定的泛型参数、需要编译器自动推断的泛型参数拆分到两层嵌套函数上,两层泛型的推断流程完全独立,不会出现未手动传参就回退默认值的问题。拆分遵循固定原则:需要用户手动传入的泛型放外层函数,需要编译器自动推断的泛型放内层函数。
基础实现
function useTest<TElement extends Element = Element>() { return <TObj extends Record<string, unknown>>(obj: TObj) => { const keys = Object.fromEntries( Object.keys(obj).map(key => [key, true]) ) as Record<keyof TObj, true> const element = document.createElement('div') as TElement return [keys, element] as const } }
调用方式与类型表现:
const demoObj = { one: 1, two: 2 } // 不指定泛型:仅多一对空括号,类型推断完全正常 const [keys1, el1] = useTest()(demoObj) // el1 类型为 Element,keys1 类型为 Record<"one" | "two", true> // 显式指定第一个泛型:无需传入第二个泛型,自动从入参推断TObj const [keys2, el2] = useTest<HTMLDivElement>()(demoObj) // el2 类型为 HTMLDivElement,keys2 类型正确为 Record<"one" | "two", true>
兼容原调用方式的重载实现
如果不想在无泛型指定的场景下多写一对空括号,可以通过函数重载同时兼容两种调用模式:
// 重载1:直接传入对象调用,全量泛型自动推断 function useTest<TObj extends Record<string, unknown>>( obj: TObj ): readonly [Record<keyof TObj, true>, Element] // 重载2:显式指定元素类型,返回待传入对象的柯里化函数 function useTest<TElement extends Element>(): <TObj extends Record<string, unknown>>( obj: TObj ) => readonly [Record<keyof TObj, true>, TElement] // 实现逻辑 function useTest<TElement extends Element = Element, TObj extends Record<string, unknown> = {}>( obj?: TObj ) { if (obj) { const keys = Object.fromEntries(Object.keys(obj).map(k => [k, true])) as Record<keyof TObj, true> const el = document.createElement('div') as TElement return [keys, el] as const } return (innerObj: Record<string, unknown>) => { const keys = Object.fromEntries(Object.keys(innerObj).map(k => [k, true])) as Record<keyof typeof innerObj, true> const el = document.createElement('div') as TElement return [keys, el] as const } }
两种调用方式均能得到正确类型:
const demoObj = { one: 1, two: 2 } // 原调用方式无改动 const [keysA, elA] = useTest(demoObj) // 显式指定泛型的调用无冗余类型标注 const [keysB, elB] = useTest<HTMLDivElement>()(demoObj)
内容的提问来源于stack exchange,提问作者SebKas
相关产品推荐
相关产品推荐

