TypeScript:更新联合类型中的公共属性时出错

时间:2019-07-04 11:15:56

标签: typescript generics

我具有以下联合类型。

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 个答案:

答案 0 :(得分:2)

我认为,如果我们删除返回类型注释并查看编译器对返回类型的看法,那么我们将对编译器在此处产生原因的理解得到最佳理解。

// 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属性,它将覆盖{{1}的value属性}。 tt的联合,它将覆盖联合的每个成员中的MyUnion

但是问题来了。您要添加的value包含value的跨度。但这本身就是t.valueMyUnion)中所有可能值的并集。结果是现在原始联合的每个成员都与所有可能值的联合配对,从而使其无法分配给原始联合。 ??‍♂️?‍♂️

对于仿制药而言,故事大相径庭。编译器不能只替换联合中的MyUnion['value']属性,因为函数内部不知道value。在3.2(PR)之前的扩展操作中使用泛型类型实际上是一个错误。从3.2开始,行为已更改为使用相交类型,乍一看应该等效。因此,让我们看一下编译器认为通用版本的返回类型是:

T

我们看到,我们添加的// 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

公平地说,非通用版本是类型更安全的版本。由于ts不会跟踪变量之间的关系,因此无法判断您的工作不是这样:

const updateDescription = (t: MyUnion): MyUnion => Object.assign({}, t, {
    value: {
        ...t.value,
        commonProp2: 'foo',
    },
});

从类型的角度来看,单个参数版本与此两个参数版本相同,但是由于两个参数的const updateDescription = (t: MyUnion, tOther: MyUnion) => ({ ...t, value: { ...tOther.value, commonProp2: 'foo', }, }); 字段不必重合,因此该参数显然不是类型安全的。