Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

typescript interface require one of two properties to exist

Tags:

typescript

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?
like image 863
Nix Avatar asked Oct 18 '22 01:10

Nix


People also ask

How do I make my properties optional in TypeScript?

Use the Partial utility type to make all of the properties in a type optional, e.g. const emp: Partial<Employee> = {}; . The Partial utility type constructs a new type with all properties of the provided type set to optional. Copied!

Can we have interface with default properties in TypeScript?

To set default values for an interface in TypeScript, create an initializer function, which defines the default values for the type and use the spread syntax (...) to override the defaults with user-provided values.

Can TypeScript interface have methods?

A TypeScript Interface can include method declarations using arrow functions or normal functions, it can also include properties and return types. The methods can have parameters or remain parameterless.

How do I inherit an interface in TypeScript?

Interfaces and Inheritance An interface can be extended by other interfaces. In other words, an interface can inherit from other interface. Typescript allows an interface to inherit from multiple interfaces. Use the extends keyword to implement inheritance among interfaces.


2 Answers

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}
    
like image 274
KPD Avatar answered Oct 19 '22 14:10

KPD


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;
like image 169
ssube Avatar answered Oct 19 '22 15:10

ssube