You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TypeScript显式指定首个泛型时如何推断第二个泛型的键类型?

问题现象

泛型函数useTest初始定义包含两个泛型参数:

  • TElement:约束为继承Element,默认值为Element
  • TObj:约束为继承{},默认值为{}

函数接收一个对象类型入参,返回const断言的二元组:

  1. 第一个元素是遍历入参所有键生成的、值固定为true的Record<keyof TObj, true>类型映射
  2. 第二个元素为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 21:51:36