为什么 A | B 允许两者结合,我该如何预防?

Tom*_*uer 16 xor typescript typescript-types

我很惊讶地发现 TypeScript 不会抱怨我做这样的事情:

type sth = { value: number, data: string } | { value: number, note: string };
const a: sth = { value: 7, data: 'test' };
const b: sth = { value: 7, note: 'hello' };
const c: sth = { value: 7, data: 'test', note: 'hello' };
Run Code Online (Sandbox Code Playgroud)

我想也许value被选为类型联合判别式或其他东西,因为我唯一能想出解释这一点的是,TypeScript 是否以某种方式number在这里理解1 | 2为例如的超集。

所以我改变valuevalue2在第二对象上:

type sth = { value: number, data: string } | { value2: number, note: string };
const a: sth = { value: 7, data: 'test' };
const b: sth = { value2: 7, note: 'hello' };
const c: sth = { value: 7, data: 'test', note: 'hello' };
Run Code Online (Sandbox Code Playgroud)

尽管如此,没有抱怨,我能够构建c. c但是,IntelliSense 崩溃了,当我.进入它时它不会提出任何建议。同样,如果我改变valuecvalue2

为什么这不会产生错误?显然,我没有提供一种或另一种类型,而是提供了两种类型的奇怪组合!

jca*_*alz 19

问题Microsoft/TypeScript#14094 中的讨论与此处相关。

TypeScript 中的类型是开放的,因为对象必须至少具有类型描述的属性才能匹配。所以对象{ value: 7, data: 'test', note: 'hello' }匹配 type { value: number, data: string },即使它有多余的note属性。所以你的c变量确实是一个有效的sth. sth如果缺少联合的某些组成部分所需的所有属性,它只会失败:

// error: missing both "data" and "note"
const oops: sth = { value: 7 };  
Run Code Online (Sandbox Code Playgroud)

但是:当您在 TypeScript 中为类型化变量分配新的对象字面量时,它会执行过多的属性检查以尝试防止错误。这具有在该分配期间“关闭”TypeScript 的开放类型的效果。这与您对接口类型的期望一样有效。但是对于联合,TypeScript 目前(如本评论中所述)只抱怨没有出现在任何组成部分上的属性。所以以下仍然是一个错误:

// error, "random" is not expected:
const alsoOops: sth = { value: 7, data: 'test', note: 'hello', random: 123 };
Run Code Online (Sandbox Code Playgroud)

但是 TypeScript 目前并没有以您想要的严格方式对联合类型进行过多的属性检查,它会根据每个组成类型检查对象字面量,并在所有这些类型中都有额外的属性时发出抱怨。它确实对受歧视的 unions执行此操作,但这并不能解决您的问题,因为两者的定义sth都不受歧视(意思是:具有一个属性,其文字类型恰好选择了联合的一个组成部分)。


因此,直到并且除非此更改,否则对您来说最好的解决方法可能是在使用对象文字时避免联合,方法是明确分配给预期的成分,然后如果需要,稍后将其扩大到联合:

type sthA = { value: number, data: string };
type sthB = { value: number, note: string };
type sth = sthA | sthB;

const a: sthA = { value: 7, data: 'test' };
const widenedA: sth = a;
const b: sthB = { value: 7, note: 'hello' };
const widenedB: sth = b;
const c: sthA = { value: 7, data: 'test', note: 'hello' }; // error as expected
const widenedC: sth = c; 
const cPrime: sthB = { value: 7, data: 'test', note: 'hello' }; // error as expected
const widenedCPrime: sth = cPrime; 
Run Code Online (Sandbox Code Playgroud)

如果你真的想表达对象类型的独占联合,你可以使用映射类型和条件类型来做到这一点,通过将原始联合变成一个新联合,其中每个成员通过添加它们来明确禁止联合其他成员的额外键作为 type 的可选属性never(显示为undefined因为可选属性始终可以是undefined):

type AllKeys<T> = T extends unknown ? keyof T : never;
type Id<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
type _ExclusifyUnion<T, K extends PropertyKey> =
    T extends unknown ? Id<T & Partial<Record<Exclude<K, keyof T>, never>>> : never;
type ExclusifyUnion<T> = _ExclusifyUnion<T, AllKeys<T>>;
Run Code Online (Sandbox Code Playgroud)

有了这个,你可以“排除”sth到:

type xsth = ExclusifyUnion<sth>;
/* type xsth = {
    value: number;
    data: string;
    note?: undefined;
} | {
    value: number;
    note: string;
    data?: undefined;
} */
Run Code Online (Sandbox Code Playgroud)

现在会出现预期的错误:

const z: xsth = { value: 7, data: 'test', note: 'hello' }; // error!
/* Type '{ value: number; data: string; note: string; }' is not assignable to
 type '{ value: number; data: string; note?: undefined; } | 
 { value: number; note: string; data?: undefined; }' */
Run Code Online (Sandbox Code Playgroud)

Playground 链接到代码

  • 有时,打字稿似乎有一个秘密积分系统,其工作方式如下。受歧视的联盟(尤其是部分分离的联盟)很容易陷入这些陷阱。 (2认同)

dan*_*nvk 11

另一种选择是使用可选never属性来明确禁止联合中两种类型的字段的混合:

type sth =
  { value: number, data: string; note?: never; } |
  { value: number, note: string; data?: never; };

const a: sth = { value: 7, data: 'test' };
const b: sth = { value: 7, note: 'hello' };
const c: sth = { value: 7, data: 'test', note: 'hello' };
   // ~ Type '{ value: number; data: string; note: string; }'
   //     is not assignable to type 'sth'.
Run Code Online (Sandbox Code Playgroud)

ts-essentials库有一个XOR泛型,可用于帮助您构建像这样的独占联合:

import { XOR } from 'ts-essentials';

type sth = XOR<
  { value: number, data: string; },
  { value: number, note: string; }
>;

const a: sth = { value: 7, data: 'test' };
const b: sth = { value: 7, note: 'hello' };
const c: sth = { value: 7, data: 'test', note: 'hello' };
// ~ Type '{ value: number; data: string; note: string; }'
//     is not assignable to type ...
Run Code Online (Sandbox Code Playgroud)

这是最后一个示例的操场链接