Apologies for the long delay between posts. I've been busy getting married and climbing. With winter setting in and the wedding behind me, I hope that I'll find more time for writing.
"Do you want coffee or tea?"
"Yes"
In ordinary speech, "or" means "exclusive or." Only programmers and logicians use an inclusive or.
In TypeScript, it's easy to get mixed up between these two:
|
We usually read the last line as "Type Thing
is a ThingOne
or ThingTwo
." But just like JavaScript's runtime or (||
), TypeScript's type-level or (|
) is an inclusive or. There's no reason a thing can't be both a ThingOne
and a ThingTwo
:
|
Why does this work? It's because TypeScript has a structural type system. Both the ThingOne
and ThingTwo
types allow additional properties that aren't declared in their interface (this fact is sometimes obscured by excess property checking):
|
So what if you really do want an exclusive or? What if you want to keep your ThingOne
s and ThingTwo
s separate? How can you model that?
There's a standard trick, which is to use an optional never
type in your interface to disallow a property:
|
Now none of the assignments from before pass the type checker (see playground):
|
This works because no value is assignable to a never
type. But because the property is optional, there's exactly one way out: not having that property.
This isn't just useful for unions. If you want to define a two dimensional vector type, for example, you might want to specifically disallow adding a third dimension:
|
With this type, you'll get an error if you accidentally pass a three-dimensional vector to a function like norm
:
|
This wouldn't be an error without the z?: never
because the call is structurally valid, even though it's semantically incorrect.
"Tags" are another common way to make an or exclusive:
|
A string can't be both "one" and "two", so there's no overlap between these types. This means there's no distinction between inclusive and exclusive or. This is one of many great reasons to use tagged unions when you can.
It's a fun exercise to define an exclusive or generic:
|
Here's what I came up with:
|
This is roughly the same as the XOR
implementation from the ts-essentials library.
There's one caveat to the optional never
trick that's worth knowing: unless you set the --exactOptionalPropertyTypes
compiler flag (added in TS 4.4), you're allowed to assign undefined
to an optional never field:
|
So remember: in TypeScript, "or" is a union: A | B
means either A
, B
, or both. Remember that "both" is a possibility, and you should either prevent it or handle it in your code.