Nix
Nix

Reputation: 58632

typescript interface require one of two properties to exist

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?

Upvotes: 280

Views: 212232

Answers (11)

Dane
Dane

Reputation: 892

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 type 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.

Upvotes: 7

LingSamuel
LingSamuel

Reputation: 584

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.

Upvotes: 18

dmwong2268
dmwong2268

Reputation: 3605

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"
}

Upvotes: 6

ford04
ford04

Reputation: 74800

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 
  1. 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.

Upvotes: 147

Wayne Baylor
Wayne Baylor

Reputation: 169

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;
  //...
}

Upvotes: 1

Mathias Piletti
Mathias Piletti

Reputation: 51

Yet another solution:

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

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

Upvotes: 3

Mohamed Allal
Mohamed Allal

Reputation: 20980

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!

Upvotes: 0

KPD
KPD

Reputation: 5893

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}
    

Upvotes: 380

Nix
Nix

Reputation: 58632

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.

Upvotes: 9

Ruan Mendes
Ruan Mendes

Reputation: 92334

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 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

Upvotes: 21

ssube
ssube

Reputation: 48337

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;

Upvotes: 222

Related Questions