这里与 Microsoft/TypeScript#14094 讨论的内容相关。
TypeScript 中的类型是“开放”的,这意味着对象必须至少具有类型描述的属性才能匹配。因此,对象 { value: 7, data: 'test', note: 'hello' }
匹配类型 { value: number, data: string }
,即使它具有多余的属性 note
:
const obj = { value: 7, data: 'test', note: 'hello' };
const val: sthA = obj;
所以你的c
变量确实是一个有效的sth
。只有当它缺少某个联合成员所需的所有属性时,才无法成为sth
:
// error: missing both "data" and "note"
const oops: sth = { value: 7 };
然而:当你在TypeScript中为一个类型变量分配一个新的对象字面量时,它会执行
过度属性检查以尝试防止错误。这将"关闭"TypeScript在该赋值期间的开放类型。对于接口类型,它按照你的预期工作。但对于联合类型,TypeScript目前(如
此评论所述)只会抱怨不出现在
任何组成部分上的属性。因此,以下仍然是一个错误:
// error, "random" is not expected:
const alsoOops: sth = { value: 7, data: 'test', note: 'hello', random: 123 };
但是 TypeScript 目前在联合类型上并不会以你想要的严格方式进行多余属性检查,即它不会将对象字面量与每个成分类型进行比较,并在所有成分类型中存在额外属性时报错。正如
microsoft/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' };
const widenedC: sth = c;
const cPrime: sthB = { value: 7, data: 'test', note: 'hello' };
const widenedCPrime: sth = cPrime;
如果您真的想表达对象类型的独占联合,您可以使用
mapped和
conditional类型来实现,将原始联合转换为一个新联合,在其中每个成员都通过将其作为
never
类型的可选属性添加到联合中明确禁止其他成员的额外键(因为可选属性总是可以为
undefined
)。
type AllKeys<T> = T extends unknown ? keyof T : never;
type Id<T> = T extends infer U ? : 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
“独家化”为:
type xsth = ExclusifyUnion<sth>;
现在预期的错误将出现:
const z: xsth = { value: 7, data: 'test', note: 'hello' };
游乐场链接到代码