66

I have a method that takes a parameter. I would like TypeScript to verify that the object being passed in (at 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.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Brian Ball
  • 12,268
  • 3
  • 40
  • 51
  • I *think* what you're looking for is [Type Guards](https://basarat.gitbooks.io/typescript/content/docs/types/typeGuard.html). AFAIK TypeScript doesn't support exclusive types. –  Feb 08 '17 at 21:15
  • For people looking to keep only one property among all the keys of an interface, I suggest using [oneOf](https://stackoverflow.com/questions/62591230/typescript-convert-a-tagged-union-into-an-union-type) – Flavien Volken Dec 05 '20 at 14:31

8 Answers8

81

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
smac89
  • 39,374
  • 15
  • 132
  • 179
Guilherme Agostinelli
  • 1,432
  • 14
  • 13
  • 8
    That XOR is so handy! How could this be extendd to support multiple mutualy exclusive types? Could you extend your answer to support `XOR` ? – maninak Feb 21 '19 at 00:46
  • 2
    One would need to build up an according chain (I suspect it works because of transitivity?!): `type XOR3 = XOR>;` – Newlukai Oct 16 '19 at 10:15
  • 11
    @maninak A little late to the party, but [this](https://preview.tinyurl.com/y9wcpxuk) (TS playground) would accomplish what you were looking for with `OneOf<[Person, Pet, Car, Tree, ..., House]>` – tjjfvi May 27 '20 at 00:34
  • 3
    Horrifying @tjjfvi... :) – Silas Davis Jun 18 '20 at 13:09
  • 2
    @tjjfvi Did you write that? I'd like to be able to credit the creator when I use it :) – Connor Dooley Oct 25 '21 at 17:03
  • 9
    @ConnorDooley Yes, I did. It was a while ago, so here's a cleaner version: https://tsplay.dev/wgLpBN – tjjfvi Oct 26 '21 at 22:09
17

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 ts-xor. I found that I needed to implement the XOR type from repo to repo all the time. So I published it for the community and me 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.

maninak
  • 2,633
  • 2
  • 18
  • 33
4

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
Ryan Cavanaugh
  • 209,514
  • 56
  • 272
  • 235
3

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
Žilvinas Rudžionis
  • 1,954
  • 20
  • 28
1

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
Siddharth Bhatt
  • 613
  • 6
  • 7
  • 1
    Works fine only until you don't have a common property in both of them, say `perimeter`. Then, you won't be able to pass neither a valid Circle nor a valid Polygon into the thing ;) – Sergiy Pereverziev Jul 21 '22 at 14:09
  • Correct. I thought that was the whole reason for this type to exist. I personally use this trick in my front-end projects to define strictly mutually exclusive props for a particular component which I can then go and spread. – Siddharth Bhatt Jul 22 '22 at 15:45
  • 1
    Yeah, that would definitely be one of the use cases, but I stumbled into this post while looking for a way to structure a tree graph, say, with leaf nodes that have value, or branch nodes that have children, but also leaves and branches could have properties in common, inherited from BaseNode. So in a resulting structure I wanted XOR, so to guarantee there can't be children and value at the same time. I solved it through an explicit `never` prop BTW, so `children?: never` in a Leaf and `value?: never` in a Branch and it worked fine. – Sergiy Pereverziev Jul 23 '22 at 21:19
1

For a bigger list of options you can use StrictUnion:

type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends any ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>

type WhatYouWanted = StrictUnion<Person | Pet | Car>
user5480949
  • 1,410
  • 1
  • 15
  • 22
0

I've come up with this solution. We have an unexported base type that we use as a blueprint for the exported type, in which only one property is allowed, and the others are never.

type OnlyOne<Base, Property extends keyof Base> = Pick<Base, Property>
& Partial<Record<keyof Omit<Base, Property>, never>>;

TS Playground

gRizzlyGR
  • 306
  • 2
  • 9
-3

You can use union types:

function getOrigin(value: Person | Pet) { }

But the last statement won't be an error:

getOrigin({ethnicity: 'abc', breed: 'def'}); // fine!

If you want that to be an error then you'll need to use overloading:

function getOrigin(value: Pet);
function getOrigin(value: Person);
function getOrigin(value: Person | Pet) {}
Nitzan Tomer
  • 155,636
  • 47
  • 315
  • 299
  • 2
    Union types doesn't solve the problem and is kinda what the OP is asking what to replace it with, but function overload works very nicely for this use case. – Meligy May 20 '18 at 15:47