Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript: union and intersection "inverted" for object keys

Tags:

typescript

From what I know of set theory, when you take a union of two sets, you overlay two sets and take the resulting set. When you take the intersection of two sets, you overlay two sets and take the parts that overlap with each other.

For numbers this works just fine:

type SomeUnion = (1 | 2 | 3) | (2 | 3 | 4)
const someUnion: SomeUnion // typeof someUnion is 1 | 2 | 3 | 4

type SomeIntersect = (1 | 2 | 3) & (2 | 3 | 4) 
const someIntersect: SomeIntersect // typeof someIntersect is 2 | 3

For object keys the intersection and union operates work quite unintuitively in my opinion.

type ObjUnion = { one: string, two: string } | { two: string, three: string }
const objUnionKeys: keyof ObjUnion // typeof objUnionKeys is 'two' while I would expect it to be all keys -> 'one' | 'two' | 'three'

type ObjIntersection = { one: string, two: string } & { two: string, three: string }
const objIntersectionKeys: keyof ObjIntersection // typeof objIntersectionKeys is 'one' | 'two' | 'three' while I would expect it only to be the keys in common -> 'one'

I imagine that there is a solid reason for why it works like this. Can anyone fill me in?

like image 676
Stefan Wullems Avatar asked Aug 31 '20 15:08

Stefan Wullems


1 Answers

You're right that the behaviour of objects unions and intersections paradoxically seems to behave in the exact opposite way to that of literals but it actually makes perfect sense :) let's dive into why!

If you have a union type for string or number literals à la

type SomeUnion = 1 | 2 | 3 | 2 | 3 | 4

what you're saying is that SomeUnion could be any one of these numbers.

When you have

type SomeIntersect = (1 | 2 | 3) & (2 | 3 | 4)

what you're saying is that SomeIntersect has to satisfy the constraints of both groups. In this case the only numbers that satisfy both are 2 and 3, hence the above is equivalent to type SomeIntersect = 2 | 3


The semantics of unions and intersections are different for objects though.

When you have

type ObjUnion = { one: string, two: string } | { two: string, three: string }

What you're saying is that ObjUnion could have either shape - meaning that the only field you know for sure exists on ObjUnion is "two". The others may or may not exist depending on which of the two shapes it actually is. But you do have certainty that { two: string } exists on the object.


When it comes to

type ObjIntersection = { one: string, two: string } & { two: string, three: string }

what you're saying is that ObjIntersection has to have all 3 fields in the two object types, otherwise it wouldn't satisfy the constraints of the intersection.

This means that if you have an object of type ObjIntersection you know that it has all 3 fields and so TypeScript will let you access any one of them without a problem!

like image 127
Aron Avatar answered Oct 20 '22 23:10

Aron