80

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.

2
  • I think what you're looking for is Type Guards. AFAIK TypeScript doesn't support exclusive types. Commented Feb 8, 2017 at 21:15
  • For people looking to keep only one property among all the keys of an interface, I suggest using oneOf Commented Dec 5, 2020 at 14:31

8 Answers 8

96

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
Sign up to request clarification or add additional context in comments.

6 Comments

That XOR is so handy! How could this be extendd to support multiple mutualy exclusive types? Could you extend your answer to support XOR<Person, Pet, Car, Tree, ..., House> ?
One would need to build up an according chain (I suspect it works because of transitivity?!): type XOR3<S, T, U> = XOR<S, XOR<T, U>>;
@maninak A little late to the party, but this (TS playground) would accomplish what you were looking for with OneOf<[Person, Pet, Car, Tree, ..., House]>
Horrifying @tjjfvi... :)
@ConnorDooley Yes, I did. It was a while ago, so here's a cleaner version: tsplay.dev/wgLpBN
|
23

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

You can even XOR more than one types like so:

XOR<A, B, C, D, E, F> // supports XORing up to 200 type arguments

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.

Comments

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

Comments

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

Comments

4

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>

Comments

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

3 Comments

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 ;)
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.
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<Branch, Leaf>, 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.
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

Comments

-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) {}

1 Comment

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.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.