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 "fresh" 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