TypeScript 是否支持对象类型之间真正的 XOR 联合?

Does TypeScript support a true XOR union between object types?

我正在尝试为一个对象定义一个类型,该对象可以是一组道具或(同一组道具和所有另一组道具)。特别是,我无法理解为什么 TypeScript 认为 props2 的值是有效的,因为它不匹配类型联合的任何一侧:

type CommonProps = {a:number, b: number};
type OptionalProps = {c: number, d: number};
type Props = CommonProps | (CommonProps & OptionalProps)

const props1: Props = {a: 1, b: 2};                   // Should be valid and is
const props2: Props = {a: 1, b: 2, c: 3};             // Shouldn't be valid but is???
const props3: Props = {a: 1, b: 2, x: 3};             // Shouldn't be valid and indeed isn't
const props4: Props = {a: 1, b: 2, c: 3, d: 4};       // Should be valid and is
const props5: Props = {a: 1, b: 2, c: 3, d: 4, e: 5}; // Shouldn't be valid and indeed isn't

在尝试理解 TypeScript 的联合方法时,我也 运行 遇到了 props4 中类似的意外行为:

type CommonProps = {a:number, b: number};
type OptionalProps1 = {c: number};
type OptionalProps2 = {d: number};
type Props = CommonProps & (OptionalProps1 | OptionalProps2)

const props1: Props = {a: 1, b: 2};                   // Shouldn't be valid and indeed isn't
const props2: Props = {a: 1, b: 2, c: 3};             // Should be valid and is
const props3: Props = {a: 1, b: 2, x: 3};             // Shouldn't be valid and indeed isn't
const props4: Props = {a: 1, b: 2, c: 3, d: 4};       // Shouldn't be valid but is?
const props5: Props = {a: 1, b: 2, c: 3, d: 4, e: 5}; // Shouldn't be valid and indeed isn't

在评估联合时,TypeScript 似乎允许对象在独立的每个字段基础上进行匹配,并且只强制类型在读取时遵循单个“路径”(如在可区分的联合中)。所以不清楚 TypeScript 联合是表示 XOR 运算还是简单的 OR 运算。

在尝试复制真正的 XOR 操作时,我尝试了(基于第一个示例):

type Props = (CommonProps & Record<keyof OptionalProps, never>) | (CommonProps & OptionalProps);

...但这也不起作用。

是否可以在 TypeScript 中定义一个类型,它只接受两种排他类型中的任何一种(而不是它们的混合类型)并且只允许读出这两种排他类型中的一种?

这是因为默认情况下不区分 typescript 联合。

type CommonProps = {a:number, b: number};
type OptionalProps = {c: number, d: number};
type Props = CommonProps | (CommonProps & OptionalProps)

const props2: Props = {a: 1, b: 2, c: 3}; // Shouldn't be valid but is???

以上类型对应CommonProps,因为它已经有ab属性。 c 属性 没有考虑在内。因此 {a: 1, b: 2, c: 3} 可分配给 {a:number, b: number} 这就是它有效的原因。

类似的情况:

type CommonProps = {a:number, b: number};
type OptionalProps1 = {c: number};
type OptionalProps2 = {d: number};
type Props = CommonProps & (OptionalProps1 | OptionalProps2)

const props4: Props = {a: 1, b: 2, c: 3, d: 4};       // Shouldn't be valid but is?

Props 应包含 ab 以及 cd。这个值 {a: 1, b: 2, c: 3, d: 4} 可以分配给 Props 因为它拥有所有这些。您可以删除 cd,它仍然有效。

您正在寻找

// credits goes to 
type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> =
  T extends any
  ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;

type StrictUnion<T> = StrictUnionHelper<T, T>

type CommonProps = { a: number, b: number };
type OptionalProps1 = { c: number };
type OptionalProps2 = { d: number };

type Props = CommonProps & StrictUnion<(OptionalProps1 | OptionalProps2)>

const props4: Props = { a: 1, b: 2, c: 3 }; //ok
const props4_1: Props = { a: 1, b: 2, d: 3 }; //ok
const props4_2: Props = { a: 1, b: 2, d: 3, c: 4 }; // expected error

Playground

如果你想使用 discriminated unions,你应该向联合中的每个类型添加相同的 属性(例如 kind)并分配唯一类型。