泛型 - TypeScript:在联合类型中更新公共属性时出错
我有以下联合类型。
type Common = { commonProp: string };
type MyFoo = Common & { foo: number };
type MyBar = Common & { bar: number };
type MyUnion = { tag: 'MyFoo'; value: MyFoo } | { tag: 'MyBar'; value: MyBar };
declare const myUnion: MyUnion;
如果我写一个函数来更新所有联合成员共有的属性(commonProp),我会得到一个错误:
// Error
/*
Type '{ value: { commonProp: string; foo: number; } | { commonProp: string; bar: number; }; tag: "MyFoo"; } | { value: { commonProp: string; foo: number; } | { commonProp: string; bar: number; }; tag: "MyBar"; }' is not assignable to type 'MyUnion'.
Type '{ value: { commonProp: string; foo: number; } | { commonProp: string; bar: number; }; tag: "MyFoo"; }' is not assignable to type 'MyUnion'.
Type '{ value: { commonProp: string; foo: number; } | { commonProp: string; bar: number; }; tag: "MyFoo"; }' is not assignable to type '{ tag: "MyFoo"; value: MyFoo; }'.
Types of property 'value' are incompatible.
Type '{ commonProp: string; foo: number; } | { commonProp: string; bar: number; }' is not assignable to type 'MyFoo'.
Type '{ commonProp: string; bar: number; }' is not assignable to type 'MyFoo'.
Property 'foo' is missing in type '{ commonProp: string; bar: number; }' but required in type '{ foo: number; }'.ts(2322)
*/
const updateDescription = (t: MyUnion): MyUnion => ({
...t,
value: {
...t.value,
commonProp: 'foo',
},
});
为什么会出现这个错误?
我似乎可以通过将函数设为泛型(受限于联合类型)来解决这个问题,但我不明白为什么这在之前的非泛型函数不起作用时会有效。
// No error
const updateCommonProp = <T extends MyUnion>(t: T): T => ({
...t,
value: {
...t.value,
commonProp: 'foo',
},
});
为什么通用函数能工作,而非通用函数不能?
答案#1
我认为,如果我们去掉返回类型注解,看看编译器如何看待返回类型,我们就能最好地了解编译器在这里是如何推理的。
// If no return type, updateDescription is inferred to:
const updateDescription: (t: MyUnion) => {
value: {
commonProp: string;
foo: number;
} | {
commonProp: string;
bar: number;
};
tag: "MyFoo";
} | {
value: {
commonProp: string;
foo: number;
} | {
commonProp: string;
bar: number;
};
tag: "MyBar";
}
Typescript 不会以任何方式跟踪变量或属性之间的关系。因此,当你展开 t 时,结果是一个与 t 类型相同的东西,然后你添加一个 value 属性,这将覆盖 t 的 value 属性。t 是 MyUnion 联合类型,它会覆盖联合类型中每个成员的 value。
但问题来了。你添加的 value 包含了 t.value 的展开。但它本身将是 MyUnion 中所有可能值的联合类型(MyUnion['value'])。结果就是,原来联合类型的每个成员现在都与所有可能值的联合类型配对,这使得它无法赋值给原来的联合类型。😞🤦♂️🤷♂️
对于泛型来说,情况就大不相同了。编译器不能仅仅替换联合类型中的 value 属性,因为在函数内部 T 是未知的。实际上,在 3.2 之前,在展开操作中使用泛型类型是一个错误(PR)。自 3.2 起,这种行为被更改为使用交叉类型,乍一看,这应该是等效的。所以让我们看看编译器认为泛型版本的返回类型是:
// If no return type ts infers for updateCommonProp:
const updateCommonProp: <T extends MyUnion>(t: T) => T & {
value: {
commonProp: string;
foo: number;
} | {
commonProp: string;
bar: number;
};
}
我们看到我们添加的 value 属性生成了和之前相同的联合类型。然而,这可以赋值给 T,因为根据定义,交叉类型可以赋值给其任何一个组成类型。所以,实际上如果我们合并交叉类型的结果可能不可赋值,这并不重要,重要的是我们正在赋值给交叉类型的一个成员。
另一种变通方法是利用这个交叉赋值规则,而不使用泛型函数。我们可以直接使用 Object.assign,它返回参数的交叉类型,而不需要像扩展运算符那样进行类型处理:
const updateDescription = (t: MyUnion): MyUnion => Object.assign({}, t, {
value: {
...t.value,
commonProp2: 'foo',
},
});
公平地说,非泛型版本是更类型安全的版本。由于 TypeScript 不跟踪变量之间的关系,它无法判断你正在做的事情不是这个:
const updateDescription = (t: MyUnion, tOther: MyUnion) => ({
...t,
value: {
...tOther.value,
commonProp2: 'foo',
},
});
从类型的角度来看,单参数版本与这个双参数版本是相同的,但显然这个版本不是类型安全的,因为两个参数的type字段不必一致。
