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
Playground
* 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
let x: TA = {a:1, b:2, c:3};
will flag up the extrac
property,let y = {a:1, b:2, c:3}; let x: TA = y;
will not. – ComparativeB -> B[P]
andA -> A[P]
, not the other way around for it to properly work when properties have different types – Hazeghi