看起来“OneOf”的意思是“必须匹配完全一个”,而“AnyOf”的意思是“必须匹配至少一个”。原来,“至少一个”是一个更基础的概念,对应|符号所代表的union操作(“inclusive or”)。因此,您的问题的答案只是:
type AnyOfSample = HasName | HasEmail // Union type
与交集的进一步联合不会改变接受的值:
type AnyOfSample = HasName | HasEmail | (HasName & HasEmail)
因为联合只能添加元素,而HasName & HasEmail 的所有元素都已经存在于HasName | HasEmail 中。观察:
type HasName = { name: string }
type HasEmail = { email: string };
type AnyOfIntersection = HasName | HasEmail | (HasName & HasEmail)
type AnyOfWithout = HasName | HasEmail
const aI: AnyOfIntersection = { name: "", email: "" }; // of course
const aW: AnyOfWithout = { name: "", email: "" }; // also accepted
如果您要使用 the in operator to narrow values,您可能希望保留交集,这会产生技术上不正确但通常有用的假设,即如果密钥未知出现在类型,那么它不存在:
function processAnyOf(aI: AnyOfIntersection, aW: AnyOfWithout) {
if (("name" in aI) && ("email" in aI)) {
aI; // (HasName & HasEmail)
}
if (("name" in aW) && ("email" in aW)) {
aW; // never
}
}
当然,这意味着您对OneOfSample 的定义不正确。这个操作更像disjunctive union ("exclusive or"),虽然不完全是因为当你有三个或更多集合时,析取联合的通常定义意味着“匹配一个奇数”,这不是你想要的。顺便说一句,我找不到我们在这里讨论的分离联合类型的广泛使用的名称,尽管这里有一个讨论它的interesting paper。
那么,我们如何在 TypeScript 中表示“完全匹配”?这并不简单,因为它最容易根据类型的 negation 或 subtraction 构建,TypeScript 目前无法做到这一点。也就是说,你想说这样的话:
type OneOfSample = (HasName | HasEmail) & Not<HasName & HasEmail>; // Not doesn't exist
但是这里没有Not。因此,您所能做的就是某种解决方法……那么有什么可能呢?你可以告诉 TypeScript 一个类型可能没有特定的属性。例如NoFoo 类型可能没有foo 键:
type ProhibitKeys<K extends keyof any> = {[P in K]?: never};
type NoFoo = ProhibitKeys<'foo'>; // becomes {foo?: never};
您可以使用条件类型获取一个键名列表并从另一个列表中删除键名(即减去字符串文字):
type Subtract = Exclude<'a'|'b'|'c', 'c'|'d'>; // becomes 'a'|'b'
这使您可以执行以下操作:
type AllKeysOf<T> = T extends any ? keyof T : never; // get all keys of a union
type ProhibitKeys<K extends keyof any> = {[P in K]?: never }; // from above
type ExactlyOneOf<T extends any[]> = {
[K in keyof T]: T[K] & ProhibitKeys<Exclude<AllKeysOf<T[number]>, keyof T[K]>>;
}[number];
在这种情况下,ExactlyOneOf 需要一个类型元组,并将表示元组中每个元素的联合,明确禁止其他类型的键。让我们看看它的实际效果:
type HasName = { name: string };
type HasEmail = { email: string };
type OneOfSample = ExactlyOneOf<[HasName, HasEmail]>;
如果我们用 IntelliSense 检查OneOfSample,它是:
type OneOfSample = (HasEmail & ProhibitKeys<"name">) | (HasName & ProhibitKeys<"email">);
也就是说“HasEmail 没有name 属性,或者HasName 没有email 属性。它有效吗?
const okayName: OneOfSample = { name: "Rando" }; // okay
const okayEmail: OneOfSample = { email: "rando@example.com" }; // okay
const notOkay: OneOfSample = { name: "Rando", email: "rando@example.com" }; // error
看起来像。
元组语法允许您添加三个或更多类型:
type HasCoolSunglasses = { shades: true };
type AnotherOneOfSample = ExactlyOneOf<[HasName, HasEmail, HasCoolSunglasses]>;
这检查为
type AnotherOneOfSample = (HasEmail & ProhibitKeys<"name" | "shades">) |
(HasName & ProhibitKeys<"email" | "shades">) |
(HasCoolSunglasses & ProhibitKeys<"email" | "name">)
如您所见,它正确地分配了被禁止的密钥。
还有其他方法可以做到这一点,但这就是我的做法。这是一种变通方法,而不是完美的解决方案,因为在某些情况下它无法正确处理,例如具有相同键但属性不同的两种类型:
declare class Animal { legs: number };
declare class Dog extends Animal { bark(): void };
declare class Cat extends Animal { meow(): void };
type HasPetCat = { pet: Cat };
type HasPetDog = { pet: Dog };
type HasOneOfPetCatOrDog = ExactlyOneOf<[HasPetCat, HasPetDog]>;
declare const abomination: Cat & Dog;
const oops: HasOneOfPetCatOrDog = { pet: abomination }; // not an error
在上面,ExactlyOneOf<> 无法递归到 pet 属性的属性以确保它不是Cat 和Dog。这可以解决,但它开始变得比您可能想要的更复杂。还有其他边缘情况。这取决于你需要什么。
Playground link to code