TypeScript直接使用类型与带extends的泛型为何表现不同?
问题描述
我搞不懂为什么用泛型和直接用具体类型写的函数,在类型推断上表现完全不一样。这个问题源自Stack Overflow上的回答《How to create a relation between parameters of a function?》。
代码示例:
interface Data { a: number, b: { x: number, y: number} } type GetKeyValueTuples<T> = { [Key in keyof T]: [Key, T[Key]] }[keyof T]; function getChangedDataByProperty<T extends Data>( ...[data, changedProp, newPropValue]: [T, ...GetKeyValueTuples<T>] ) { if (changedProp === "b") { return { ...data, b: { // 疑问:这里newPropValue的类型为什么是number、"a"、"b"的联合?不是应该是{b的类型}吗? x: newPropValue.x, y: newPropValue.y, } } } else { return { ...data, x: newPropValue } } } // 疑问:为什么这个不用泛型的函数,类型推断就正常? function getChangedDataByProperty2( ...[data, changedProp, newPropValue]: [Data, ...GetKeyValueTuples<Data>] ) { if (changedProp === "b") { return { ...data, b: { x: newPropValue.x, y: newPropValue.y, } } } else { return { ...data, x: newPropValue } } }
差异原因拆解
非泛型函数的精准推断
getChangedDataByProperty2直接用Data作为类型参数,此时GetKeyValueTuples<Data>会被直接解析成具体的联合类型:
["a", number] | ["b", { x: number, y: number }]
当你用changedProp === "b"做条件判断时,TypeScript能准确做类型窄化——它知道一旦changedProp是"b",newPropValue必然对应{ x: number, y: number },所以访问x、y完全没问题。
泛型函数的类型局限
getChangedDataByProperty用了T extends Data,这里的T可以是Data的任意子类型(比如你可以传入一个把b改成其他类型的对象)。TypeScript无法提前确定T的具体结构,所以GetKeyValueTuples<T>会被推断成一个宽泛的联合:
[keyof T, T[keyof T]]
当你判断changedProp === "b"时,TypeScript没法确定T里的"b"对应的类型是不是和原始Data一致(毕竟T可以重定义b),所以newPropValue的类型依然是T[keyof T](也就是T["a"] | T["b"]的联合),自然没法直接访问x、y。
修复方案
要让泛型函数实现参数间的类型绑定,得把键和对应值的关联明确写出来,比如改成这种写法:
function getChangedDataByProperty<K extends keyof Data>( data: Data, changedProp: K, newPropValue: Data[K] ) { if (changedProp === "b") { return { ...data, b: { ...newPropValue } } } else { return { ...data, [changedProp]: newPropValue } } }
这里直接把泛型参数K绑定到Data的键上,newPropValue的类型就是Data[K],TypeScript能准确识别窄化后的类型,不会再出现错误。
内容的提问来源于stack exchange,提问作者Znar

