TypeScript中任意数量类型的非析取并集

时间:2020-07-02 14:29:41

标签: typescript typescript-generics

在TypeScript中,我想创建一个表示属于一个或更多不同类型的值的并集类型,类似于OpenAPI或{{3}中的oneOf }。根据{{​​3}},TypeScript中的union运算符应该执行此操作,因为它表示集合并集(包括或)。但是,这与JSON Schema时的TypeScript中的类型推断行为不符,后者似乎假定值完全属于联合类型之一(分离联合或互斥或)。举一个简单的例子,a previous answer on a similar question

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();
    }
}

为了获得我想要的行为和using the in operator as a type guard,我必须将交集类型的所有组合手动添加到并集:

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> = ...;

但是我可以使用类似AnyOf<A | B>实用程序类型leads to a type error中的技术,制作类似的泛型类型,采用任意数量的类型,例如作为联合(UnionToIntersection

1 个答案:

答案 0 :(得分:0)

出现错误的原因是,使用联合类型不能正确地将SoftwareDeveloperOrChefSoftwareDeveloper都设为Chef。如果我们知道对象是SoftwareDeveloper,那么我们根本没有关于cook属性的任何信息-它可以是任何东西。它不一定是预期类型的​​函数。

type AnyOf2<A, B> = A | B | A & B;几乎就在那里,但是当我们处理对象的属性时,我们可以使用PartialRequired获得更好的类型。 / p>

我们希望为SoftwareDeveloperOrChefOrBoth实现的最后一种类型是cookcode都是可选函数,但至少需要一个。

尝试一下:

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

type SoftwareDeveloperOrChef = EitherOrBoth<SoftwareDeveloper, Chef>;

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

但是此代码签出了

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

修改

根据我们在评论中的讨论,您希望各个接口SoftwareDeveloperChef具有多种方法,并且您希望能够确定是否Chef一种方法是目前,Chef的所有其他方法也将存在。

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

此实用程序类型Not<T>通过说T的任何键都不能设置为undefined以外的任何值来处理此问题:

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

请注意,仅不包括该属性并不表示无法设置该属性。如linked thread中所述,对于您的type AorB,您在创建新的无效对象时会出错(该对象具有SoftwareDeveloper的所有方法,而{{1 }}),但在将类型分配给现有对象时却不能,因为该无效对象可分配给Chef,因此也可分配给A

这不是以前建议的宽松的AorB类型,而是强制执行排他性的更严格的类型:

EitherOrBoth<A,B>

使用这种类型,不再只是假设一个人可以type ExclusiveAndOr<A, B> = A & Not<B> | B & Not<A> | A & B; type SoftwareDeveloperOrChef = ExclusiveAndOr<SoftwareDeveloper, Chef>; ,他们也可以cook

chop

Typescript Playground Link