typescript - 允许多个不同形状的接口(interface)作为 TypeScript 返回类型

标签 typescript interface unions typeguards

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

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 语句保证返回 T1A 或 T2A 之一。在我的实际用例中,我有更多类型和参数,但我分配它们的方式总是保证至少返回一个指定的接口(interface)。在处理接口(interface)时,似乎联合并不是真正的“这个或这个”。我试图分解我的代码来处理每个单独的类型,但是当有 8 种不同的可能性时,我最终会得到很多看起来无用的额外 if/else block 。

我也尝试过使用类型 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 专门针对 discriminated unions 的情况进行上述计算。如果您的联合具有可用于区分联合成员的属性(这意味着 string literalsnumeric literalsundefinednull 等单例类型)在 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

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

如果希望 T1A | T2A | T1B | T2B 成为可区分的联合,则需要修改组成类型的定义,以便真正通过至少一个公共(public)属性进行区分。比如说,这个:
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 类型的可选属性,您现在可以使用 typenameindex 作为判别式。 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

关于typescript - 允许多个不同形状的接口(interface)作为 TypeScript 返回类型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60172520/

相关文章:

c - FreeBSD:网络接口(interface)信息

c++ - c\c++ - 以 union 作为参数以满足特定要求的函数

c - 如何在 C 中访问 union 内的结构成员?

javascript - Visual Studio Code - 缺少大多数 JavaScript 语法突出显示

typescript - 如何最好地模拟 Typescript 中的命名空间?

java - 如何将多个接口(interface)分组为一个通用的单个接口(interface)?

c - 如何在 D 中创建可以与 C 代码接口(interface)的连续多维数组?

typescript - 如何在 Electron 中使用 typescript 而无需编译?

javascript - 未提供 HTMLScriptElement 的所有属性时出现 typescript 错误

C++ 匿名结构