An example:
type TA = {a:number,b:number}
type TB = {a:number,c:number,d:number}
const t1:Or<TA,TB> = {a:1,b:1} // want
const t2:Or<TA,TB> = {a:1,c:1,d:1} // want
const t3:Or<TA,TB> = {a:1,b:1,c:1} // DON'T want
The desired result is for t1
to be valid because it exactly fits TA
, and t2
to be valid because it exactly fits TB
, but for t3
to be invalid because it doesn't exactly fit either TA
or TB
.
When
type Or<T,U>=T|U
TypeScript actually considers t3
to be valid. TypeScript union type |
is also allowing keys objects to be merged and sometimes partial objects.
Placing each type in a single element array as follows:
type T0 = {a:number,b:number}
type T1 = {a:number,c:number,d:number}
type Test=[T0]|[T1]
const t1:Test=[{a:1,b:1,}]
const t2:Test=[{a:1,c:1,d:1}]
const t3:Test=[{a:1,b:1,c:1}] // fails as desired
works, but the object under test also has to be placed in a single-element array.
Is there any way to get around that?
The Why
In TypeScript, |
is not an operator*, it denotes that the type is a union of lefthand and righthand-side types. This is important to understand if you are to grasp why {a:number,b:number} | {a:number,c:number,d:number}
allows {a:number,b:number,c:number}
.
When you declare a union, you tell the compiler that a type assignable to the union should be assignable to at least one member of it. With this in mind, let's check the {a:number,b:number,c:number}
type from this point of view.
The lefthand side member of the union is {a:number,b:number}
, which means that types assignable to it must have at least 2 properties of type number
: a
and b
(there is a notion of excess property checks for object literals, but, as already mentioned by T.J. Crowder, this is inapplicable for unions). From the handbook**:
the compiler only checks that at least the ones required are present and match the types required
Thus, since {a:number,b:number,c:number}
is assignable to {a:number,b:number}
no more checks are needed - the type satisfies at least one requirement of the union. Btw, this behavior is perfectly in line with the truth table of the logical OR, which is analogous to what a union is.
Your attempt to resolve this by wrapping the types into tuples relies on naked vs. wrapped type parameter behavior. Because you wrapped the types in tuples, the compiler compares tuples of one element to each other. Obviously, the third tuple is not the same as the first and the second one, which gives you the desired result.
The What
What you actually want is the behavior exhibited by logical XOR: one of, but not both. Apart from using tagged types (mentioned by T.J. Crowder), one can define a utility transforming a pair of types into a union of "all props from A that are present in both, but not in A alone" types:
type XOR<A,B> = ({ [ P in keyof A ] ?: P extends keyof B ? A[P] : never } & B) | ({ [ P in keyof B ] ?: P extends keyof A ? B[P] : never } & A);
And here is how it would work (the trade-off of the utility is that the excess property is leaked to intellisense, but one is immediately disallowed to specify it due to never
):
const t0:XOR<TA,TB> = {a:1} //property 'b' is missing
const t1:XOR<TA,TB> = {a:1,b:1} // OK
const t2:XOR<TA,TB> = {a:1,c:1,d:1} // OK
const t3:XOR<TA,TB> = {a:1,b:1,c:1} // types of property 'c' are incompatible
* The notion of |
being an operator was present in the first revision and was later edited out
** It has to be noted that this does not mean all checks shortcircuit when a match is found. In this case, all members of the union are object literals themselves, so the constraint on known properties still applies to each, leading to the TS2322 error if an unknown property is present during assignment:
const t4:XOR<TA,TB> = {a:1,b:1,c:1, g:3} //Object literal may only specify known properties