1

Problem Description

I want to define two optional properties in an interface in Typescript. One and only one of these two properties must be present in the instance object of this interface.

What I have tried

interface ISidebarCommon {
  /**
   * The label to be used in sidebar
   */
  label: string;
  /**
   * Icon class in string format for the icon of the sidebar image
   */
  icon: string;
}

interface IRoutableSidebarItem extends ISidebarCommon {
  /**
   * Role number to determine which route to redirect the user to
   * This property is mutually exclusive with children
   */
  role: number;
}

interface ITreeSidebarItem<SI> extends ISidebarCommon {
  /**
   * An array of children sidebar items.
   * This property is mutually exclusive with role
   */
  children: SI[];
}

interface ISidebar {
  [index: number]: IRoutableSidebarItem | ITreeSidebarItem<IRoutableSidebarItem
    | ITreeSidebarItem<IRoutableSidebarItem>
  >;
}

Problem with current solution

While the current solution makes sure that one of the two properties, i.e, role and children, must be present, it does not make them mutually exclusive. That is, both role and children may be present in the instance object and it will still pass the current interface check.

Sample of Problem

The following is an example of an instance of the ISidebar interface where the objects contain both role and children and the linter still doesn't show any errors:

const sidebarBroken: ISidebar = [
  {
    label: 'l1',
    icon: 'c1',
    role: 5,
    children: [
      {
        label: 'l2',
        icon: 'c2',
        role: 6,
        children: [
          {
            label: 'l3',
            icon: 'c3',
            role: 7,
          },
        ],
      },
    ],
  },
];
Aayush Sharma
  • 779
  • 4
  • 20
  • 1
    Looks like the same problem [as this question](https://stackoverflow.com/questions/65843563/union-type-allows-wrong-assignment-with-properties-from-the-used-types) –  Jan 22 '21 at 11:06
  • That question is more focused on the workings of Union type in Typescript while what I want to know is whether or not there is a way to force mutual exclusion of types in Typescrtipt. I know that Union type is not the solution to assert mutual exclusion of types as Union type is closer to the logical or operation and what I want is closer to the XOR gate operation for two types. – Aayush Sharma Jan 22 '21 at 11:16
  • Your answer with `never` [works](https://stackoverflow.com/questions/40510611/typescript-interface-require-one-of-two-properties-to-exist/60617060#60617060), as TypeScript can [make a discriminant property](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-2.html#non-unit-types-as-union-discriminants) out of `undefined` (`never` + optional becomes `undefined`). – ford04 Jan 22 '21 at 13:58

1 Answers1

0

I found a solution for this by making use of the never type in typescript and optional properties.

What I had to do was update my interface declarations in such a that an instance of IRoutableSidebarItem will never have a children property and an instance of ITreeSidebarItem will never have a role property.

Updated example using the above mentioned workaround:

interface ISidebarCommon {
  /**
   * The label to be used in sidebar
   */
  label: string;
  /**
   * Icon class in string format for the icon of the sidebar image
   */
  icon: string;
}

interface IRoutableSidebarItem extends ISidebarCommon {
  /**
   * Role number to determine which route to redirect the user to
   * This property is mutually exclusive with children
   */
  role: number;
  children?: never;
}

interface ITreeSidebarItem<SI> extends ISidebarCommon {
  /**
   * An array of children sidebar items.
   * This property is mutually exclusive with role
   */
  children: SI[];
  role?: never;
}

interface ISidebar {
  [index: number]: IRoutableSidebarItem | ITreeSidebarItem<IRoutableSidebarItem
    | ITreeSidebarItem<IRoutableSidebarItem>
  >;
}

/*
  Throws a linting error
*/
const sidebarBroken: ISidebar = [
  {
    label: 'l1',
    icon: 'c1',
    role: 5,
    children: [
      {
        label: 'l2',
        icon: 'c2',
        role: 6,
        children: [
          {
            label: 'l3',
            icon: 'c3',
            role: 7,
          },
        ],
      },
    ],
  },
];

/*
  Doesn't throw a linting error
*/
const sidebarWorking: ISidebar = [
  {
    label: 'l1',
    icon: 'c1',
    children: [
      {
        label: 'l2',
        icon: 'c2',
        children: [
          {
            label: 'l3',
            icon: 'c3',
            role: 7,
          },
        ],
      },
    ],
  },
  {
    label: 'l1',
    icon: 'c1',
    role: 12,
  },
];
Aayush Sharma
  • 779
  • 4
  • 20