typescript interface require one of two properties to exist
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;
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'>
Pick<T, Exclude<keyof T, Keys>>
fromRequireAtLeastOne
becomes{ title: string, icon: string}
, which are the unchanged properties of the keys not included in'click' | 'component'
{ [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>> }[Keys]
fromRequireAtLeastOne
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}
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}