在 TypeScript 中,我想创建一个联合类型来表示属于一个或多个不同类型的值,类似于 oneOf
在 OpenAPI或 JSON Schema .根据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> = ...;
但是我可以使用类似于 AnyOf<A | B>
中使用的技术来制作一个采用任意数量类型的类似泛型类型,例如联合 ( UnionToIntersection
)实用类型 implemented here ?
最佳答案
出现错误的原因是使用联合类型不能正确地允许 SoftwareDeveloperOrChef
两者都是 SoftwareDeveloper
和 Chef
.如果我们知道对象是 SoftwareDeveloper
那么我们根本没有关于 cook
的任何信息属性(property)——可以是任何东西。它不必是预期类型的函数。
您的 type AnyOf2<A, B> = A | B | A & 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: () => {};}
是我们类型的有效成员。
但是这段代码 check out
const softwareDeveloperOrChef: SoftwareDeveloperOrChef = person;
if (softwareDeveloperOrChef.code) {
softwareDeveloperOrChef.code();
if (softwareDeveloperOrChef.cook) {
// this is allowed now
softwareDeveloperOrChef.cook();
}
}
编辑
根据我们在评论中的讨论,您需要单独的接口(interface) SoftwareDeveloper
和 Chef
有多种方法,您希望能够确定 Chef
中的一种方法是否有效存在,Chef
的所有其他方法也将出席。
为了最大限度的类型安全,组合 SoftwareDeveloperOrChefOrBoth
type 应该明确声明任何成员必须拥有 Chef
的所有方法或者没有。
此实用程序类型 Not<T>
通过说 T 的任何键不能设置为 undefined
以外的任何值来处理此问题:
export type Not<T> = {
[P in keyof T]?: never;
};
请注意,仅不包括该属性并不表示无法设置它。如 linked thread 中所述, 和你的 type AorB
当创建一个新的无效对象(一个具有 SoftwareDeveloper
的所有方法和一些但不是所有 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
.
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 - TypeScript 中任意数量类型的非析取联合,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62698675/