TypeScript泛型函数将泛型接口类型折叠为unknown问题排查
我写了一个合并多个API请求结果的函数,规则是所有请求无错误时合并data,只要有一个错误就返回该错误:
mergeAPIResults({ data: 123 }, { data: 'test' }) // 输出: { data: [ 123, 'test' ] } mergeAPIResults({ data: 123 }, { error: 'failed' }) // 输出: { error: 'failed' }
实际用的是React Query的useQuery(),这里简化成了最简示例。问题是:一旦扩展原接口或使用鸭子类型的兼容接口,TypeScript会把推导后的类型折叠成unknown。
正常工作的代码
以下代码的类型推导完全符合预期:
interface APIResult<DataType> { data?: DataType error?: any } // 映射类型:将[ number, string, ... ]转为[ APIResult<number>, APIResult<string>, ... ] type WrapWithAPIResults<T extends any[]> = { [K in keyof T]: APIResult<T[K]> } function mergeAPIResults<T extends any[]>(...apiResults: WrapWithAPIResults<T>): APIResult<T> { const error = apiResults.find(r => r.error)?.error if (error) { return { error } } else { return { data: apiResults.map(r => r.data) as T } } } const mergeTest = mergeAPIResults({ data: 123 }, { data: 'test' }) // ✅ 推导类型: APIResult<[number, string]> const mergeTest2 = mergeAPIResults(mergeTest, { data: 456 }) // ✅ 推导类型: APIResult<[[number, string], number]>
出现类型推导问题的场景
当使用扩展原接口的类型时,TypeScript无法正确推导data的类型,会变成unknown:
interface ExtendedAPIResult<DataType> extends APIResult<DataType> { extraData: string } const extApiResult: ExtendedAPIResult<string> = { data: 'test', extraData: 'blah' } const mergeTestError = mergeAPIResults({ data: 456 }, extApiResult) // ❌ 推导类型: APIResult<[number, unknown]> // 期望类型: APIResult<[number, string]>
哪怕移除extraData、创建和原接口完全一致的APIResult2,问题依然存在。但用React Query的UseQueryResult却能正常推导:
const mergeTestRQ = mergeAPIResults({ data: 456 }, useQuery({ queryFn: () => true })) // ✅ 推导类型: APIResult<[number, boolean]>
问题原因&解决方案
原因
你的泛型约束是WrapWithAPIResults<T>,也就是让TypeScript从传入的APIResult<T[K]>实例反向推导T。但当传入的是APIResult的子类型(比如ExtendedAPIResult<string>)时,TypeScript的类型推导会选择最宽泛的匹配:ExtendedAPIResult<string>既符合APIResult<string>,也符合APIResult<unknown>(因为string兼容unknown)。此时TypeScript会默认选unknown,导致最终推导结果异常。
而React Query的UseQueryResult能正常工作,是因为它的泛型参数是显式绑定的,useQuery返回的是明确的UseQueryResult<boolean, Error>类型,TypeScript能直接提取到data的类型,不会触发宽泛匹配的问题。
解决方案
修改函数的泛型定义,直接从每个输入参数的类型中提取data的类型,而不是依赖反向推导:
interface APIResult<DataType> { data?: DataType error?: any } function mergeAPIResults<Results extends APIResult<any>[]>( ...apiResults: Results ): APIResult<{ [K in keyof Results]: Results[K]['data'] }> { const error = apiResults.find(r => r.error)?.error if (error) { return { error } as APIResult<never> } else { return { data: apiResults.map(r => r.data) as any } } } // 测试扩展接口的场景 interface ExtendedAPIResult<DataType> extends APIResult<DataType> { extraData: string } const extApiResult: ExtendedAPIResult<string> = { data: 'test', extraData: 'blah' } const mergeTestFixed = mergeAPIResults({ data: 456 }, extApiResult) // ✅ 推导类型: APIResult<[number, string]>
这种写法直接遍历输入参数数组的每个元素,提取其data字段的类型,不管输入是APIResult还是它的子类型,都能正确推导。
内容的提问来源于stack exchange,提问作者blade

