【问题标题】:Union and Intersection of types类型的并集和交集
【发布时间】:2020-04-30 12:05:06
【问题描述】:

为了练习,我写了这段代码:

type Cat = {name: string, purrs: boolean};
type Dog = {name: string, barks: boolean, bites: boolean};

type CatOrDogOrBoth = Cat | Dog;
type CatAndDog = Cat & Dog;

let pet1 : CatOrDogOrBoth = {name: "pooky", purrs: true, bites: false};
let pet2 : CatAndDog = {name: "Timmy" };

但是 pet2 上的 TypeScript 编译器错误说我需要将 purrs 添加到我的对象中。 但是CatAndDog 不是这两种类型的交集,所以只有 name 属性吗?

【问题讨论】:

  • 两种类型A & B的交集是一个C,这样每个C都是一个A每个C都是一个@987654331 @。对于每个CatAndDog 要成为Cat,它必须具有purrs 属性;没有如果,没有但是。我认为您的困惑来自于假设交集类型的属性应该是两个原始类型的属性的交集;这通常不是真的。

标签: typescript


【解决方案1】:

你有交集和联合倒退,这在人们学习 TypeScript 时并不罕见。造成这种混淆的一个可能原因是对象类型在其键的类型中是contravariant,因此对象类型的交集具有其键的并集,反之亦然。即keyof (Cat & Dog)(keyof Cat) | (keyof Dog)相同,keyof (Cat | Dog)(keyof Cat) & (keyof Dog)相同:

type KeyExploration = {
  keysOfIntersection: keyof (Cat & Dog) // "name" | "purrs" | "barks" | "bites"
  unionOfKeys: keyof Cat | keyof Dog // "name" | "purrs" | "barks" | "bites"
  keysOfUnion: keyof (Cat | Dog) // "name"
  intersectionOfKeys: (keyof Cat) & (keyof Dog) // "name"
}

由于这种逆变性,如果您对对象类型的概念等同于其声明的属性集,您将混淆交集和并集。相反,您应该将类​​型视为允许值的集合(有关更多信息,请参阅this answer)。您应该将类​​型的并集和交集视为可分配给这些类型的值集的并集和交集。

对于像"foo" 这样的literal type,该集合只有一个元素:{"foo"}。对于像 string 这样的类型,它是(实际上)所有 JavaScript 字符串的无限集合:{ x | typeof x === "string" }(使用 set builder notation)。对于像{y: 0} 这样的对象类型,它也是一个无限集:{x | x.y === 0 }... 即所有具有 y 属性的 JavaScript 对象的集合,该属性完全等于 0


另一个可能的混淆来源是 TypeScript 中的对象类型是 open 而不是 closedexact(请参阅 microsoft/TypeScript#12936 以获取请求对于确切的类型)。

对象类型定义显示哪些属性必须存在,但它们没有讨论哪些属性不能存在。一个对象可能具有比其类型定义中提到的更多的属性:

interface Garfield extends Cat {
  hatesMondays: true,
  eatsLasagna: true,
}
declare const garfield: Garfield;
const garfieldAsACat: Cat = garfield; // okay

(由于excess property checks 的存在,这有点复杂,它将“新鲜”对象字面量视为精确类型。但此类检查是例外而不是规则。)

由于对象类型是开放的,这意味着可分配值的集合比您想象的要大。 {a: 0}{b: 1} 这样的两个对象类型实际上有很大的重叠;例如,值{a: 0, b: 1, c: 2, d: 3} 可以分配给它们。


现在让我们考虑一下交集 (&) 和并集 (|):

如果我有一个 Cat & Dog 类型的对象,它必须可以分配给 both Cat Dog。因为对象类型是开放的,所以没有什么说 Cat 不能有 barksbites 属性。并没有说Dog 不能有purrs 属性。因此,如果您有一个既是 Cat 又是 Dog 的东西,它必须具有这两种类型的所有属性

let okay1: CatAndDog = 
  { name: "CatDog", purrs: true, bites: true, barks: true }; // Cat and Dog

pet2 失败,因为它既不是Cat 也不是Dog

let pet2: CatAndDog = { name: "Timmy" }; // neither Cat nor Dog

另一方面,Cat | Dog 类型的对象只能分配给 either Cat Dog。如果您为 Cat | Dog 类型的变量赋值,则它至少需要是其中之一:

let okay1: CatOrDogOrBoth = 
  { name: "Sylvester", purrs: false }; // Cat
let okay2: CatOrDogOrBoth = 
  { name: "Odie", barks: true, bites: false }; // Dog

您的pet1 是可以接受的,因为它是Cat。它有一个额外的bites 属性,这很好(并且不会被过多的属性检查发现,尽管有些人认为应该这样做(参见microsoft/TypeScript#20863):

let pet1: CatOrDogOrBoth = 
  { name: "pooky", purrs: true, bites: false }; // Cat with bites:false

如果我有一个Cat | Dog 类型的对象并且我还没有检查它以查看它是CatDog 中的哪一个,那么我可以访问的唯一安全属性是它的name,因为这是我知道肯定会出现的唯一属性。 可能Cat | Dog 将同时具有这两种类型的一些属性,正如您对pet1 的初始化所表明的那样,但您不能保证这一点。


Link to code

【讨论】:

    猜你喜欢
    • 2019-07-26
    • 2015-07-16
    • 1970-01-01
    • 1970-01-01
    • 2011-07-14
    • 1970-01-01
    • 1970-01-01
    • 2018-05-07
    相关资源
    最近更新 更多