Home > OS >  Multiple exclusive union type
Multiple exclusive union type

Time:07-16

In the following code:

interface IPositionChange {
    previousIndex: number;
    newIndex: number;
}

interface IVisibilityChange {
    index: number;
    panel: string;
}

interface IAction {
    action: 'paint' | 'clean';
}

interface IActionChange extends IAction, IVisibilityChange { }

// type IChange = (IVisibilityChange | IActionChange) & Partial<Record<keyof IPositionChange, never>>
//     | IPositionChange & Partial<Record<keyof IActionChange, never>> & Partial<Record<keyof IVisibilityChange, never>>;

// type IChange = IVisibilityChange & Partial<Record<keyof IPositionChange, never>> & Record<keyof IAction, never>
//     | IPositionChange & Partial<Record<keyof IActionChange, never>> & Partial<Record<keyof IVisibilityChange, never>>
//     | IActionChange & Partial<Record<keyof IPositionChange, never>>;

// type IChange = IVisibilityChange | IActionChange | IPositionChange;

function isPositionChange(change: IChange): change is IPositionChange {
    return !!change.previousIndex;
}

function isVisibilityChange(change: IChange): change is IVisibilityChange {
    return !!change.panel && !change.action;
}

function isActionChange(change: IChange): change is IActionChange {
    return !!change.panel && !!change.action;
}

function thisShouldNotCompile(change: IChange) {
    change = {
        previousIndex: 1,
        newIndex: 2,
        // either the ones above or the one below should throw and error,
        // as they should be exclusive.
        index: 0,
        panel: 'asd',
        action: 'paint',
    }
}

I am trying to make IActionChange not accept properties from multiple types, but only from a single one, i've commented some of my previous attempts and none seems to be working properly, as there is always some condition that is failed.

Im probably missing something pretty simple but can't quite put my finger on it, what is wrong with all these approaches ?

EDIT:

Changing the interfaces to this would cover all the case scenarios, and would make them exclusive, but i would rather not repeat unnecessary properties, as this would get more and more verbose as new properties are added:

interface IPositionChange {
  previousIndex: number;
  newIndex: number;
  action: never;
  index: never;
  panel: never;
}

interface IVisibilityChange {
  index: number;
  panel: string;
  previousIndex: never;
  newIndex: never;
  action: never;
}

interface IAction {
  action: 'paint' | 'clean';
  index: never;
  panel: never;
  previousIndex: never;
  newIndex: never;
}

CodePudding user response:

There is actually an issue in the Typescript repository here regarding this.

One of the answers proposed there is

type Without<T> = { [P in keyof T]?: undefined };
type XOR<T, U> = (Without<T> & U) | (Without<U> & T);

Now your code can be rewritten as

type Without<T> = { [P in keyof T]?: undefined };
type XOR<T, U> = (Without<T> & U) | (Without<U> & T);
type IChange = XOR<IPositionChange, IActionChange> | XOR<IVisibilityChange, IActionChange> | XOR<IPositionChange, IVisibilityChange>;

Explanation:

The Without type is expecting all the properties of T to be absent.

The XOR type is expecting all the properties of T to be present, and all the properties of U to be absent, or vice versa.

From there, you can use this to create a 3-way XOR.

CodePudding user response:

Would something likes this work for you ? It uses the in operator to narrow the types in your functions.

interface IPositionChange {
  previousIndex: number;
  newIndex: number;
}

interface IVisibilityChange {
  index: number;
  panel: string;
}

interface IAction {
  action: 'paint' | 'clean';
}

interface IActionChange extends IAction, IVisibilityChange { }

type IChange = IVisibilityChange | IActionChange | IPositionChange;

function isPositionChange(change: IChange): change is IPositionChange {
  return 'previousIndex' in change;
}

function isVisibilityChange(change: IChange): change is IVisibilityChange {
  return 'panel' in change && 'action' in change && !change.action;
}

function isActionChange(change: IChange): change is IActionChange {
  return 'panel' in change && 'action' in change;
}
  • Related