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

2023-12-07

我惊讶地发现 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' };

我想也许value被选为类型联合判别式或其他什么,因为我唯一能想到的解释就是 TypeScript 是否能够理解number这里是一个超集1 | 2例如。

所以我改变了value to be value2在第二个对象上:

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' };

尽管如此,没有抱怨,我能够构建c。 IntelliSense 崩溃c不过,当我.进去。如果我改变也一样value in c to be value2.

为什么这不会产生错误?显然,我未能提供其中一种类型,而是提供了两种类型的奇怪组合!


讨论中的问题微软/TypeScript#14094在这里相关。

TypeScript 中的类型有open从某种意义上说,一个对象必须具有at least类型所描述的属性以使其匹配。所以对象{ value: 7, data: 'test', note: 'hello' }匹配类型{ value: number, data: string },即使它有多余的note财产:

const obj = { value: 7, data: 'test', note: 'hello' };
const val: sthA = obj; // okay

So your c变量确实是有效的sth。它只会失败sth如果是的话missing工会某些组成部分所需的所有属性:

// error: missing both "data" and "note"
const oops: sth = { value: 7 };  

但是:当您将新的对象文字分配给 TypeScript 中的类型变量时,它会执行超额财产检查尽量避免错误。这具有在该赋值期间“关闭”TypeScript 的开放类型的效果。这正如您对接口类型所期望的那样。但对于工会来说,TypeScript 目前(如这条评论)仅抱怨未出现的属性any的成分。所以下面的仍然是一个错误:

// error, "random" is not expected:
const alsoOops: sth = { value: 7, data: 'test', note: 'hello', random: 123 };

但 TypeScript 目前并没有按照您想要的严格方式对联合类型进行过多的属性检查,它会根据每个组成类型检查对象文字,并抱怨所有类型中是否存在额外的属性。它确实做到了这一点受歧视的工会,如中提到的微软/TypeScript#12745,但这并不能解决你的问题,因为这两个定义都没有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; 

If you really想要表达一个独家的对象类型的联合,您可以使用mapped and 有条件的类型来做到这一点,通过将原始联合转换为新联合,其中每个成员通过将它们添加为 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>>;

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

type xsth = ExclusifyUnion<sth>;
/* type xsth = {
    value: number;
    data: string;
    note?: undefined;
} | {
    value: number;
    note: string;
    data?: undefined;
} */

现在会出现预期的错误:

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; }' */

Playground 代码链接

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么A | B 允许两者结合,我该如何防止呢? 的相关文章

随机推荐