typescript interface require one of two properties to exist

Typescript

Typescript Problem Overview


I'm trying to create an interface that could have

export interface MenuItem {
  title: string;
  component?: any;
  click?: any;
  icon: string;
}
  1. Is there a way to require component or click to be set
  2. Is there a way to require that both properties can't be set?

Typescript Solutions


Solution 1 - Typescript

With the help of the Exclude type which was added in TypeScript 2.8, a generalizable way to require at least one of a set of properties is provided is:

type RequireAtLeastOne<T, Keys extends keyof T = keyof T> =
    Pick<T, Exclude<keyof T, Keys>> 
    & {
        [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>
    }[Keys]

And a partial but not absolute way to require that one and only one is provided is:

type RequireOnlyOne<T, Keys extends keyof T = keyof T> =
    Pick<T, Exclude<keyof T, Keys>>
    & {
        [K in Keys]-?:
            Required<Pick<T, K>>
            & Partial<Record<Exclude<Keys, K>, undefined>>
    }[Keys]

Here is a TypeScript playground link showing both in action.

The caveat with RequireOnlyOne is that TypeScript doesn't always know at compile time every property that will exist at runtime. So obviously RequireOnlyOne can't do anything to prevent extra properties it doesn't know about. I provided an example of how RequireOnlyOne can miss things at the end of the playground link.

A quick overview of how it works using the following example:

interface MenuItem {
  title: string;
  component?: number;
  click?: number;
  icon: string;
}

type ClickOrComponent = RequireAtLeastOne<MenuItem, 'click' | 'component'>
  1. Pick<T, Exclude<keyof T, Keys>> from RequireAtLeastOne becomes { title: string, icon: string}, which are the unchanged properties of the keys not included in 'click' | 'component'

  2. { [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>> }[Keys] from RequireAtLeastOne becomes

     { 
         component: Required<{ component?: number }> & { click?: number }, 
         click: Required<{ click?: number }> & { component?: number } 
     }[Keys]
    

Which becomes

    {
        component: { component: number, click?: number },
        click: { click: number, component?: number }
    }['component' | 'click']

Which finally becomes

    {component: number, click?: number} | {click: number, component?: number}

3. The intersection of steps 1 and 2 above

    { title: string, icon: string} 
    & 
    ({component: number, click?: number} | {click: number, component?: number})

simplifies to

    { title: string, icon: string, component: number, click?: number} 
    | { title: string, icon: string, click: number, component?: number}

Solution 2 - Typescript

Not with a single interface, since types have no conditional logic and can't depend on each other, but you can by splitting the interfaces:

export interface BaseMenuItem {
  title: string;
  icon: string;
}

export interface ComponentMenuItem extends BaseMenuItem {
  component: any;
}

export interface ClickMenuItem extends BaseMenuItem {
    click: any;
}

export type MenuItem = ComponentMenuItem | ClickMenuItem;

Solution 3 - Typescript

There is a simpler solution. No need to rely on any or complex conditional types (see answer):

> 1. Is there a way to require component or click to be set? (Inclusive OR)

type MenuItemOr = {
    title: string;
    icon: string;
} & ({ component: object } | { click: boolean }) 
// brackets are important here: "&" has precedence over "|"

let testOr: MenuItemOr;
testOr = { title: "t", icon: "i" } // error, none are set
testOr = { title: "t", icon: "i", component: {} } // ✔
testOr = { title: "t", icon: "i", click: true } // ✔
testOr = { title: "t", icon: "i", click: true, component: {} } // ✔

A union type (|) corresponds to inclusive OR. It is intersected with the non-conditional properties.

Use the in operator to narrow the value back to one of the constituents:

if ("click" in testOr) testOr.click // works 

> 2. Is there a way to require that both properties can't be set? (Exclusive OR / XOR)

type MenuItemXor = {
    title: string;
    icon: string;
} & (
        | { component: object; click?: never }
        | { component?: never; click: boolean }
    )

let testXor: MenuItemXor;
testXor = { title: "t", icon: "i" } // error, none are set
testXor = { title: "t", icon: "i", component: {} } // ✔
testXor = { title: "t", icon: "i", click: true } // ✔
testXor = { title: "t", icon: "i", click: true, component: {} } //error,both set

Basically either component or click can be set, the other should never 1 be added at the same time. TS can make a discriminated union type out of MenuItemXor, which corresponds to XOR.

This XOR condition for MenuItemXor is not possible with accepted answer.


Playground

1 Technically, prop?: never gets resolved to prop?: undefined, though former is often used for illustration.

Solution 4 - Typescript

An alternative without multiple interfaces is

export type MenuItem = {
  title: string;
  component: any;
  icon: string;
} | {
  title: string;
  click: any;
  icon: string;
};

const item: MenuItem[] = [
  { title: "", icon: "", component: {} },
  { title: "", icon: "", click: "" },
  // Shouldn't this error out because it's passing a property that is not defined
  { title: "", icon: "", click: "", component: {} },
  // Does error out :)
  { title: "", icon: "" }
];

I've asked a similar question at https://stackoverflow.com/questions/48230773/how-to-create-a-partial-like-that-requires-a-single-property-to-be-set

The above could be simplified, but it may or may not be easier to read

export type MenuItem = {
  title: string;
  icon: string;
} & (
 {component: any} | {click: string}
)

Note that none of these prevent you from adding both because TypeScript does allow extra properties on objects that use AND/OR See https://github.com/Microsoft/TypeScript/issues/15447

Solution 5 - Typescript

I use this:

type RequireField<T, K extends keyof T> = T & Required<Pick<T, K>>

Usage:

let a : RequireField<TypeA, "fieldA" | "fieldB">;

This makes fieldA and fieldB required.

Solution 6 - Typescript

I ended up doing:

export interface MenuItem {
  title: string;
  icon: string;
}

export interface MenuItemComponent extends MenuItem{
  component: any;
}

export interface MenuItemClick extends MenuItem{
  click: any;
}

Then I used:

 appMenuItems: Array<MenuItemComponent|MenuItemClick>;

But was hoping there was a way to model it with a single interface.

Solution 7 - Typescript

I like using Pick along with a base type that includes all properties to establish these kinds of conditional requirements.

interface MenuItemProps {
  title: string;
  component: any;
  click: any;
  icon: string;
}

export interface MenuItem =
  Pick<MenuItemProps, "title" | "icon" | "component"> |
  Pick<MenuItemProps, "title" | "icon" | "click">

This is clean and also flexible. You can get arbitrarily complex with your requirements, asserting things like "require either all the properties, just these two properties, or just this one property" and so on while keeping your declaration simple and readable.

Solution 8 - Typescript

Here's a simple way to implement either but not both

type MenuItem =  {
  title: string;
  component: any;
  click?: never;
  icon: string;
} | {
  title: string;
  component?: never;
  click: any;
  icon: string;
}

// good
const menuItemWithComponent: MenuItem = {
  title: 'title',
  component: "my component",
  icon: "icon"
}

// good
const menuItemWithClick: MenuItem = {
  title: 'title',
  click: "my click",
  icon: "icon"
}

// compile error
const menuItemWithBoth: MenuItem = {
  title: 'title',
  click: "my click",
  component: "my click",
  icon: "icon"
}

Solution 9 - Typescript

Yet another solution:

type RequiredKeys<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>;

type MenuItem2 = RequiredKeys<MenuItem, "component" | "click">;

Solution 10 - Typescript

This approach combines never and Omit. Benefits here are that it's easy to understand and also easy to update if you need to add more properties.

interface Base {
  title: string;
  icon: string;
  component?: never;
  click?: never;
}

interface OnlyComponent {
  component: any;
}

interface OnlyClick {
  click: any;
}

export type MenuItem = (Omit<Base, 'component'> & OnlyComponent) | (Omit<Base, 'click'> & OnlyClick);

You can use in to narrow an instance of MenuItem:

const item: MenuItem = {
  title: 'A good title';
  icon: 'fa-plus';
  component: SomeComponent;
};

//...

if('component' in item) {
  const Comp = item.component;
  //...
}

Solution 11 - Typescript

To just extends upon the cool answers above! And for the people that land here while searching for a Partial version with requiring capability! Here a snippet i made to take!

PartialReq

You want to have a Partial of an interface, but in mean time require some of the fields! Here how it's done

export type PartialReq<T, Keys extends keyof T = keyof T> =
    Pick<Partial<T>, Exclude<keyof T, Keys>>
    & {
        [K in Keys]: T[K]
    };

Use example

export interface CacheObj<SigType = any, ValType = any> {
    cache: Map<SigType, ValType>,
    insertionCallback: InsertionCallback<SigType, ValType> // I want this to be required
}

// ...

export class OneFlexibleCache<SigType = any, ValType = any> {
    private _cacheObj: CacheObj<SigType, ValType>;

    constructor(
        cacheObj: PartialReq<CacheObj<SigType, ValType>, 'insertionCallback'> // <-- here
                                                                           //  i used it
    ) {
        cacheObj = cacheObj || {};

        this._cacheObj = {

// ...

// _______________ usage
this._caches.set(
    cacheSignature,
    new OneFlexibleCache<InsertionSigType, InsertionValType>({
        insertionCallback // required need to be provided
    })
);

Here you can see that it work perfectly

enter image description here

If the required not provided

enter image description here

UPDATE: For the usage that i implied above here a better answer

I just went by the doc and found Omit.

https://www.typescriptlang.org/docs/handbook/utility-types.html#omittk

I came to add it. But before i do, I just seen this cool answer. It cover all:

https://stackoverflow.com/a/48216010/7668448

Just check it out! It show how to do it for all the different version of Typescript! And for the sake of not repeating ! Go and check!

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionNixView Question on Stackoverflow
Solution 1 - TypescriptKPDView Answer on Stackoverflow
Solution 2 - TypescriptssubeView Answer on Stackoverflow
Solution 3 - Typescriptford04View Answer on Stackoverflow
Solution 4 - TypescriptJuan MendesView Answer on Stackoverflow
Solution 5 - TypescriptLingSamuelView Answer on Stackoverflow
Solution 6 - TypescriptNixView Answer on Stackoverflow
Solution 7 - TypescriptDaneView Answer on Stackoverflow
Solution 8 - Typescriptdmwong2268View Answer on Stackoverflow
Solution 9 - TypescriptMathias PilettiView Answer on Stackoverflow
Solution 10 - TypescriptWayne BaylorView Answer on Stackoverflow
Solution 11 - TypescriptMohamed AllalView Answer on Stackoverflow