Union and Intersection of types

You’ve got intersection and union backwards, which for some reason is not uncommon when people learn about TypeScript. One probable cause for this confusion is that an object type is contravariant in the type of its keys, so an intersection of object types has a union of its keys and vice versa. That is, keyof (Cat & Dog) is the same as (keyof Cat) | (keyof Dog), and keyof (Cat | Dog) is the same as (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"
}

Because of this contravariance, you will get intersections and unions mixed up if your conception of an object type is equivalent to its set of declared properties. Instead, you should think about a type as a set of allowed values (see this answer for more information). And you should think of unions and intersections of types as unions and intersections of the sets of values assignable to those types.

For a literal type like "foo", the set has a single element: { "foo" }. For a type like string, it’s the (practically) infinite set of all JavaScript strings: { x | typeof x === "string" } (using set builder notation). And for object types like {y: 0}, it’s also an infinite set: { x | x.y === 0 }… that is, the set of all JavaScript objects with a y property exactly equal to 0.


Another possible source of confusion is that object types in TypeScript are open and not closed or exact (see microsoft/TypeScript#12936 for a request for exact types).

Object type definitions show which properties must be present, but they do not talk about which properties must not be present. An object may have more properties than mentioned in its type’s definition:

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

(This is complicated a bit by the presence of excess property checks, which treat object literals as if they were exact types. But such checks are the exception and not the rule.)

Since object types are open, it means that the set of assignable values is larger than you might have thought. Two object types like {a: 0} and {b: 1} actually have significant overlap; for example, the value {a: 0, b: 1, c: 2, d: 3} is assignable to both of them.


Now let’s think about intersection (&) and union (|):

If I have an object of type Cat & Dog, it must be assignable to both Cat and Dog. Because object types are open, nothing says that a Cat cannot have a barks or a bites property. And nothing says that a Dog cannot have a purrs property. So if you have something that is both a Cat and a Dog, it must have all the properties of both types.

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

And pet2 fails because it’s neither a Cat nor a Dog:

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

On the other hand, an object of type Cat | Dog must only be assignable to either Cat or Dog. If you assign a value to a variable of type Cat | Dog it needs to be at least one of those:

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

Your pet1 is acceptable because it’s a Cat. It has an extra bites property, which is fine (and is not caught by excess property checking, although some people think it should (see microsoft/TypeScript#20863):

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

If I have an object of type Cat | Dog and I haven’t yet inspected it in order to see which of Cat or Dog it is, the only safe property I can access is its name, because that’s the only property I know for sure will be present. It is possible that a Cat | Dog will have some properties from both types, as you show by your initialization of pet1, but you can’t guarantee it.


Link to code

Leave a Comment