Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does Typescript support mutually exclusive types?

Tags:

typescript

I have a method that takes a parameter. I would like Typescript to verify that the object being passed in (at typescript compile-time, I understand run-time is a different animal) only satisfies one of the allowed interfaces.

Example:

interface Person {ethnicity: string;}
interface Pet {breed: string;}
function getOrigin(value: Person ^ Pet){...}

getOrigin({}); //Error
getOrigin({ethnicity: 'abc'}); //OK
getOrigin({breed: 'def'}); //OK
getOrigin({ethnicity: 'abc', breed: 'def'});//Error

I realize that Person ^ Pet is not valid Typescript, but it's the first thing I thought to try and seemed reasonable.

like image 674
Brian Ball Avatar asked Feb 08 '17 20:02

Brian Ball


5 Answers

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
like image 142
Guilherme Agostinelli Avatar answered Nov 07 '22 14:11

Guilherme Agostinelli


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 add tests and document it properly with a readme and jsdoc annotations. The implementation is what @Guilherme Agostinelli shared from the community.

like image 42
maninak Avatar answered Nov 07 '22 14:11

maninak


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
like image 4
Ryan Cavanaugh Avatar answered Nov 07 '22 16:11

Ryan Cavanaugh


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
like image 2
Žilvinas Rudžionis Avatar answered Nov 07 '22 15:11

Žilvinas Rudžionis


As of TS v4.7, I found Omit to be the simplest solution:

interface Circle {
    radius: number;
}

interface Polygon {
    sides: number;
}

type Either<A, B> = Omit<A, keyof B> | Omit<B, keyof A>;

const mutuallyExclusiveProps: Either<Circle, Polygon> = { radius: 5 };

mutuallyExclusiveProps.sides = 5; // Error
like image 1
Siddharth Bhatt Avatar answered Nov 07 '22 14:11

Siddharth Bhatt