【问题标题】:Non-disjunctive union of arbitrary number of types in TypeScriptTypeScript 中任意数量类型的非析取联合
【发布时间】:2020-10-23 04:34:10
【问题描述】:

在 TypeScript 中,我想创建一个联合类型来表示属于一个或多个不同类型的值,类似于OpenAPI 或JSON Schema 中的oneOf。根据a previous answer on a similar question,TypeScript 中的联合运算符应该这样做,因为它表示集合联合(包括或)。但是,这与 using the in operator as a type guard 时 TypeScript 中的类型推断行为不匹配,这似乎假定值属于完全联合类型之一(析取联合或异或)。举个简单的例子,这个leads to a type error:

interface SoftwareDeveloper { code(): void; }
interface Chef { cook(): void; }
type SoftwareDeveloperOrChef = SoftwareDeveloper | Chef;

// Determined at runtime. May have the code method, the cook method, or both.
const person = { code: () => { }, cook: () => { } };

const softwareDeveloperOrChef: SoftwareDeveloperOrChef = person;
if ("code" in softwareDeveloperOrChef) {
    // softwareDeveloperOrChef has inferred type SoftwareDeveloper
    softwareDeveloperOrChef.code();
    // Coding is hungry work
    if ("cook" in softwareDeveloperOrChef) {
        // Not allowed, since softwareDeveloperOrChef has inferred type never
        softwareDeveloperOrChef.cook();
    }
}

为了得到我想要的行为和avoid type errors,我必须手动将交集类型的所有组合添加到联合中:

type SoftwareDeveloperOrChefOrBoth = SoftwareDeveloper | Chef | (SoftwareDeveloper & Chef);
const softwareDeveloperOrChefOrBoth: SoftwareDeveloperOrChefOrBoth = person;
if ("code" in softwareDeveloperOrChefOrBoth) {
    // softwareDeveloperOrChef has inferred type SoftwareDeveloper | (SoftwareDeveloper & Chef)
    softwareDeveloperOrChefOrBoth.code();
    if ("cook" in softwareDeveloperOrChefOrBoth) {
        // Allowed, since softwareDeveloperOrChefOrBoth has inferred type SoftwareDeveloper & Chef
        softwareDeveloperOrChefOrBoth.cook();
    }
}

一个中间问题可能是这是否是我应该期待的行为?但是,鉴于这是已实现的行为,我实际上更感兴趣的是一种为任意数量的类型构造非析取联合类型的方法。随着类型数量的增加,手动执行会导致联合类型定义的大小呈指数增长:

type AorB = A | B | A & B;
type AorBorC = A | B | C | A & B | A & C | B & C | A & B & C;
type AOrBOrCorD = A | B | C | D | ... | B & C & D | A & B & C & D;

我可以为特定数量的参数编写泛型类型:

type AnyOf2<A, B> = A | B | A & B;
type AnyOf3<A, B, C> = AnyOf2<A, AnyOf2<B, C>>;
type AnyOf4<A, B, C, D> = ...;

但我能否使用UnionToIntersection 实用程序类型implemented here 中使用的技术创建一个类似的泛型类型,该类型采用任意数量的类型,例如联合(AnyOf&lt;A | B&gt;)?

【问题讨论】:

  • 从示例中,不清楚为什么需要所有交集类型的组合。要摆脱 TypeScript 错误,您可以将 intersection type 定义为 type SoftwareDeveloperOrChefOrBoth = SoftwareDeveloper &amp; Chef;
  • @mancristiana:我不能使用SoftwareDeveloper &amp; Chef 的原因是分配给变量的值是在运行时确定的,可能没有实现这两个接口。我已编辑问题以添加解释这一点的评论。

标签: typescript typescript-generics


【解决方案1】:

您遇到错误的原因是使用联合类型不能正确地允许SoftwareDeveloperOrChef 既是SoftwareDeveloper 又是Chef。如果我们知道该对象是SoftwareDeveloper,那么我们根本就没有关于cook 属性的任何信息——它可能是任何东西。它不必是预期类型的​​函数。

您的type AnyOf2&lt;A, B&gt; = A | B | A &amp; B; 几乎就在那里,但是当我们处理对象的属性时,我们可以利用Partial 和Required 来获得更好的类型。

我们希望为SoftwareDeveloperOrChefOrBoth 实现的最终类型是cook 和code 都是可选函数,但至少需要一个。

试试这个:

type EitherOrBoth<A, B> = Partial<A & B> & (A | B);

type SoftwareDeveloperOrChef = EitherOrBoth<SoftwareDeveloper, Chef>;

一个变化是,通过使用Partial,您不能再使用"code" in softwareDeveloperOrChef 作为类型保护,因为对象可以将属性键显式设置为undefined。例如{code: undefined; cook: () =&gt; {};} 是我们类型的有效成员。

但是这段代码检查出来了

const softwareDeveloperOrChef: SoftwareDeveloperOrChef = person;
if (softwareDeveloperOrChef.code) {
    softwareDeveloperOrChef.code();
    if (softwareDeveloperOrChef.cook) {
        // this is allowed now
        softwareDeveloperOrChef.cook();
    }
}

编辑

根据我们在 cmets 中的讨论,您希望单独的接口 SoftwareDeveloper 和 Chef 具有多个方法,并且您希望能够确定如果存在 Chef 的一种方法,那么所有Chef 的其他方法也会出现。

为了最大程度地保证类型安全,SoftwareDeveloperOrChefOrBoth 组合类型应明确声明任何成员必须具有 Chef 的所有方法或没有。

这个实用程序类型Not&lt;T&gt; 处理这个问题的方式是,T 的任何键都不能设置为undefined 以外的任何值:

export type Not<T> = {
    [P in keyof T]?: never;
};

请注意,简单地不包括该属性并不意味着它不能被设置。正如linked thread 中所解释的那样,使用type AorB 创建新的无效对象时会出现错误(具有SoftwareDeveloper 的所有方法和Chef 的一些但不是所有方法的对象),但在分配时不会类型为现有对象,因为该无效对象可分配给A,因此也可分配给AorB。

与之前建议的较宽松的 EitherOrBoth&lt;A,B&gt; 类型不同,这里有一个更严格的类型,强制排他性:

type ExclusiveAndOr<A, B> = A & Not<B> | B & Not<A> | A & B;

type SoftwareDeveloperOrChef = ExclusiveAndOr<SoftwareDeveloper, Chef>;

有了这种类型,不再只是假设如果一个人可以cook,他们也可以chop。

function checkPerson( softwareDeveloperOrChef: SoftwareDeveloperOrChef ) {
    if (softwareDeveloperOrChef.code) {
        softwareDeveloperOrChef.code();
        if (softwareDeveloperOrChef.cook) {
            // should be ok
            softwareDeveloperOrChef.cook();
            // ok to assume all methods exist
            softwareDeveloperOrChef.chop();
        } else {
            // should be error
            softwareDeveloperOrChef.chop();
        }
    }
}

Typescript Playground Link

【讨论】:

  • 不幸的是,正如您所指出的,使用 Partial 意味着我失去了使用类型保护的能力。例如,如果我将chop 添加到Chef,并且我检查了softwareDeveloperOrChef.cook 存在,我不能依赖chop 也存在。这让AnyOf 解决方案对我来说更有效。
  • 如果您假设一个功能基于其他功能的存在而存在,您可能会面临错误。请记住,打字稿界面并不是说您可以仅拥有这些属性。如果我创建一个对象const person = { code: () =&gt; { }, cook: () =&gt; { } },这将分配给AnyOf,因为它可以分配给SoftwareDevoloper。但是您可以检查它是否包含cook,它确实包含,但它不包括chop。所以要小心,在运行时你的对象只能是一个完整的Chef 或一个完整的SoftwareDeveloper 并且没有介于两者之间。
  • 阅读this discussion。我会根据您的要求编辑我的答案。
  • 感谢您看到我的打字错误的潜在来源。但是,我的主要问题仍然是如何将这种“非分离联合”扩展到任意数量的类型,而无需创建单独的 AnyOf2/ExclusiveAndOr2, AnyOf3/ExclusiveAndOr3, ... 类型。
猜你喜欢
  • 2017-05-07
  • 2020-02-25
  • 1970-01-01
  • 1970-01-01
  • 2019-12-14
  • 2021-10-12
  • 1970-01-01
  • 1970-01-01
  • 2021-04-23
相关资源
最近更新 更多