允许多个不同形状的接口作为 TypeScript return 类型

Allowing multiple differently shaped interfaces as TypeScript return types

我有一个函数,它接受一些参数并生成将传递到外部进程的对象。因为我无法控制最终需要创建的形状,所以我必须能够将一些不同的参数带到我的函数中,并将它们 assemble 放入适当的对象中。这是一个非常基本的示例,展示了我遇到的问题:

interface T1A {
    type: 'type1';
    index: number;
}

interface T1B {
    type: 'type1';
    name: string;
}

interface T2A {
    type: 'type2';
    index: number;
}

interface T2B {
    type: 'type2';
    name: number;
}

function hello(type: 'type1' | 'type2'): T1A | T1B | T2A | T2B {
    return {type, index: 3}
}

此特定函数抱怨 'type1' 无法分配给 'type2'。我阅读了一些有关类型保护的内容,并弄清楚了如何使这个简单的示例快乐:

function hello(type: 'type1' | 'type2'): T1A | T1B | T2A | T2B {
    if (type === 'type1') {
        return {type, index: 3}
    } else {
        return {type, index: 3}
    }
}

但是,我不明白为什么这是必要的。类型检查对 return 值的可能性完全没有任何作用。基于我的参数仅采用两个显式字符串之一的事实,单个 return 语句保证 return T1A 或 T2A 之一。在我的实际用例中,我有更多的类型和参数,但我分配它们的方式总是保证 return 至少一个指定的接口。看来union在处理接口的时候并不是真的'this OR this'。我试图分解我的代码来处理每个单独的类型,但是当有 8 种不同的可能性时,我最终得到了很多看起来无用的额外 if/else 块。

我也尝试过使用类型type T1A = {...};

我是不是对工会有什么误解,或者这是一个错误,还是更简单的处理方法?

TypeScript 通常不会执行从属性向上传播联合的类型。也就是说,虽然类型 {foo: string | number} 的每个值都应该可以分配给类型 {foo: string} | {foo: number},但编译器不会将它们视为可相互分配的值:

declare let unionProp: { foo: string | number };
const unionTop: { foo: string } | { foo: number } = unionProp; // error!

除了当您开始改变此类类型的属性时会发生奇怪的事情之外,编译器通常需要做太多的工作才能一直这样做,尤其是当您有多个联合属性时。 relevant comment in microsoft/TypeScript#12052 说:

this sort of equivalence only holds for types with a single property and isn't true in the general case. For example, it wouldn't be correct to consider { x: "foo" | "bar", y: string | number } to be equivalent to { x: "foo", y: string } | { x: "bar", y: number } because the first form allows all four combinations whereas the second form only allows two specific ones.

所以这本身不是一个错误,而是一个限制:编译器只会花这么多时间与联合一起玩,看看某些代码是否有效。


在 TypeScript 3.5 中,support was added to do the above computations specifically for the case of discriminated unions. If your union has properties that can be used to distinguish between members of the union (which means singleton types like string literals, numeric literals, undefined, or null) in at least some member of the union,编译器将按照您希望的方式验证您的代码。

这就是为什么当您将 T1A | T2A | T1B | T2B 更改为 T1A | T2A 时它突然起作用的原因,并且可以作为您问题的可能答案:

function hello(type: 'type1' | 'type2'): T1A | T2A | T1B | T2B {
    const x: T1A | T2A = { type, index: 3 }; // okay
    return x;
}

后者是有区别的联盟:type 属性 告诉你你有哪个成员。但前者不是:type属性可以区分T1A | T1BT2A | T2B,但没有属性进一步细分。不,类型中仅缺少 indexname 不算作判别式,因为类型 T1A 的值可能具有 name 属性; TypeScript 中的类型不是 exact.

上面的代码之所以有效,是因为编译器可以验证 xT1A | T2A 类型,然后可以验证 T1A | T2A 是完整 T1A | T2A | T1B | T2B 的子类型.因此,如果您对两步过程感到满意,这可能是您的解决方案。


如果想让T1A | T2A | T1B | T2B成为可区分的联合,需要修改构成类型的定义,使其真正被至少一个共同的属性区分。比如,这个:

interface T1A {
    type: 'type1';
    index: number;
    name?: never;
}

interface T1B {
    type: 'type1';
    name: string;
    index?: never;
}

interface T2A {
    type: 'type2';
    index: number;
    name?: never;
}

interface T2B {
    type: 'type2';
    name: number;
    index?: never;
}

通过添加 never 类型的可选属性,您现在可以使用 typename index作为判别式。 nameindex 属性有时会是 undefined,支持区分类型的单例类型。然后这有效:

function hello(type: 'type1' | 'type2'): T1A | T2A | T1B | T2B {
    return { type, index: 3 }; // okay
}

所以这是两个选项。在您知道某些东西是安全的但编译器不知道的情况下始终可用的另一种选择是使用 type assertion:

function hello2(type: 'type1' | 'type2') {
    return { type, index: 3 } as T1A | T2A | T1B | T2B
}

好的,希望对您有所帮助;祝你好运!

Playground link to code