Does Typescript support mutually exclusive types?
Solution 1:
As proposed in this issue, you could use conditional types to write a XOR type:
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
And now your example works:
interface Person {ethnicity: string;}
interface Pet {breed: string;}
function getOrigin(value: XOR<Person, Pet>) { /* ... */}
getOrigin({}); //Error
getOrigin({ethnicity: 'abc'}); //OK
getOrigin({breed: 'def'}); //OK
getOrigin({ethnicity: 'abc', breed: 'def'});//Error
Solution 2:
You can use the tiny npm package ts-xor
that was made to tackle this problem specifically.
With it you can do the following:
import { XOR } from 'ts-xor'
interface A {
a: string
}
interface B {
b: string
}
let A_XOR_B: XOR<A, B>
A_XOR_B = { a: 'a' } // OK
A_XOR_B = { b: 'b' } // OK
A_XOR_B = { a: 'a', b: 'b' } // fails
A_XOR_B = {} // fails
Full disclosure: I'm the author of this tiny package. I found out I needed to implement the XOR type from repo-to-repo all the time. So I published it for me and the community and in this way I could also document it properly with a readme and shared jsdoc annotations. The implementation is what @Guilherme Agostinelli shared from the community.
Solution 3:
To augment Nitzan's answer, if you really want to enforce that ethnicity
and breed
are specified mutually exclusively, you can use a mapped type to enforce absence of certain fields:
type Not<T> = {
[P in keyof T]?: void;
};
interface Person {ethnicity: string;}
interface Pet {breed: string;}
function getOrigin(value: Person & Not<Pet>): void;
function getOrigin(value: Pet & Not<Person>): void;
function getOrigin(value: Person | Pet) { }
getOrigin({}); //Error
getOrigin({ethnicity: 'abc'}); //OK
getOrigin({breed: 'def'}); //OK
var both = {ethnicity: 'abc', breed: 'def'};
getOrigin(both);//Error
Solution 4:
You can use Discriminating Unions:
interface Person {
readonly discriminator: "Person"
ethnicity: string
}
interface Pet {
readonly discriminator: "Pet"
breed: string
}
function getOrigin(value: Person | Pet) { }
getOrigin({ }) // Error
getOrigin({ discriminator: "Person", ethnicity: "abc" }) // OK
getOrigin({ discriminator: "Pet", breed: "def"}) // OK
getOrigin({ discriminator: "Person", ethnicity: "abc", breed: "def"}) // Error