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 ThingOnes and ThingTwos 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.