The Reason
Within TypeScript, the symbol |
does not function as an operator; instead, it signifies that a type is a combination of both the left and right types. Understanding this concept is crucial in comprehending why
{a:number,b:number} | {a:number,c:number,d:number}
permits
{a:number,b:number,c:number}
.
When defining a union, you are informing the compiler that any type assignable to the union must be at least assignable to one of its members. With this in mind, let's analyze the type {a:number,b:number,c:number}
from this perspective.
The left side member of the union is {a:number,b:number}
, meaning that types capable of being assigned to it must have a minimum of 2 properties with type number
: a
and b
. Referencing the handbook:
the compiler only checks that at least the ones required are present and match the types required
Therefore, since {a:number,b:number,c:number}
can be assigned to {a:number,b:number}
, no further validation is necessary - the type fulfills at least one condition of the union. This behavior aligns perfectly with the truth table of the logical OR, mirroring the functionality of a union.
Your efforts to resolve this by enclosing the types within tuples stem from the distinction between naked vs. wrapped type parameters. By utilizing tuples, the compiler assesses one-element tuples against each other. Clearly, the third tuple differs from the first two, resulting in the desired outcome.
The Concept
What you truly desire is akin to the logic of XOR: "one of, but not both." Apart from adopting tagged types (as suggested by T.J. Crowder), a utility can be crafted to transform a pair of types into a union encompassing "all properties from A that intersect with B, but not solely part of A":
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);
Here is an illustration of its application (note that although intellisense hints at excess property leakage, specifying such properties is immediately disallowed 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 claim of |
being an operator was included in the initial revision and subsequently removed.
** It is essential to note that not all validations halt when a match is identified. In scenarios where all union members are object literals, the restriction on recognized properties still applies, leading to a TS2322 error if an unknown property is detected during assignment:
const t4:XOR<TA,TB> = {a:1,b:1,c:1, g:3} //Object literal may only specify known properties