Skip to main content

typescript-mutually-exclusive-xor-type

#post

@typescript-mutually-exclusive-xor-type

  • copy in my Without and XOR code
  • link to the stack overflow post I got it from
  • Link to the TS issue discussing it
  • Link to the conditional types docs mentioned in the SO post
  • TS missing a built type helper for easily making a union type mutually exclusive
  • For that, you need each type to set the excluded properties as optional never
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
 
// A custom XOR type that declares two types to be mutually exclusive
// Used when we want to ensure we only pass one of a pair of related params to an endpoint
// Example: XOR<{ a: string }, { b: string }> becomes { a: string, b?: never } | { a?: never, b: string }
// see: https://stackoverflow.com/a/53229567/8802485
export type XOR<T, U> = T | U extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;

Alternative that uses Partial:

// see: https://stackoverflow.com/questions/42123407/does-typescript-support-mutually-exclusive-types#comment123255834_53229567
// see: https://www.typescriptlang.org/play/?#code/LAKALgngDgpgBAVQHYEsD2SDSMIGcA8AKgHxwC8chcMAHmDEgCa6VwD8cA1jmgGasAuOEhgA3GACcA3KFAB6OXACSAWygS04uCiT0ANnpS4Gx0JFhwAojSgBDJkVIUqtekxZUOAbzgBtTNpIXDz8hAC6QoT+YXAAvnBCIuLSsuDQ8ADyIhm8RNR0DMxwXrG+YU7FoHB+ATrBEHyUEVY29oxE0XAAZHAACrYSYCi2evgASjAAxmgS7daTegCujDD4yOhYOARRSIsqAEaS5QA09Y1RmCfCYpLEdzIgpbsHRw+gOvQSvLaT8ADCtjAlRA1WqRgBYCEYAkixgD1iqQ+km+vzgABE0ABzYGg7S4DGYqEwuGgBEgd66ZE-eAAIRQsxxoKMdNmRNh8NS5ngAEFUCoRuQ4FkYDl8L4IacCacWYxyg8FKCAHpsVLTJC4IGTQFCXkofl6QU+cHauDQ2FxB5qjVwRhYnV8gUUI34u2m4kW0BWzWAgn2vWO4p4iFsmCnIy+t3m2JSOAKyQaCSgIA
 
type UnionKeys<T> = T extends T ? keyof T : never;
 
// Improve intellisense
type Expand<T> = T extends T ? { [K in keyof T]: T[K] } : never;
 
type OneOf<T extends {}[]> = {
  [K in keyof T]: Expand<T[K] & Partial<Record<Exclude<UnionKeys<T[number]>, keyof T[K]>, never>>>;
}[number];
 
interface Cat {
  isCat: true;
}
 
interface Dog {
  isDog: true;
}
 
interface Bird {
  isBird: true;
}
 
type Animal = OneOf<[Cat, Dog, Bird]>;
 
// ^?
 
const cat: Animal = { isCat: true };
const dog: Animal = { isDog: true };
const catDog: Animal = { isCat: true, isDog: true }; // error

Simpler alternative that uses Omit, which removes the property instead of setting it to never:

// see: https://stackoverflow.com/a/72858846/8802485
export type Either<A, B> = Omit<A, keyof B> | Omit<B, keyof A>;

The problem with this is that both properties in the pair can be undefined at the same time. We want only one to be (when going the “include but set to undefined” route):

export type PerturbationsEndpointParams = {
  concentration: string;
  concentrationSf: string;
  filterTags: string;
  geneExpressionThreshold: string | undefined; // must include, but ok if value is undefined
  groupLabel: string;
  ignoreGeneThreshold: 'true' | 'false';
  includeControlWells: 'true' | 'false';
  pairwise: 'true' | 'false';
  perturbations: string;
  pvalue: string | undefined; // must include, but ok if value is undefined
  pvalueByPertType: string | undefined; // must include, but ok if value is undefined
  search: string;
  splitBy: string;
  url?: string;
  zfpkmByTimepoint: string | undefined; // must include, but ok if value is undefined
};

Inbox