Интерфейс typescript требует наличия одного из двух свойств

Я пытаюсь создать интерфейс, который может иметь

export interface MenuItem {
  title: string;
  component?: any;
  click?: any;
  icon: string;
}
  • Можно ли установить component или click способ
  • Есть ли способ требовать, чтобы оба свойства не могли быть установлены?

Ответ 1

Не с одним интерфейсом, так как типы не имеют условной логики и не могут зависеть друг от друга, но вы можете разделить интерфейсы:

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;

Ответ 2

С помощью типа Exclude который был добавлен в TypeScript 2.8, обеспечивается обобщенный способ требовать, по крайней мере, одного из набора свойств:

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]

И частичный, но не абсолютный способ требовать предоставления одного и только одного:

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]

= Pick> & { [K in Keys%5D-?: Required> & Partial>> }[Keys] interface MenuItem { title: string; component?: number; click?: number; icon: string; } type ClickOrComponent = RequireAtLeastOne const withComponent: ClickOrComponent = { title: "test", component: 52, icon: "icon" } const withClick: ClickOrComponent = { title: "test", click: 54, icon: "icon" } const errorWithNeither: ClickOrComponent = { title: "test", icon: "icon" } const noErrorWithBoth: ClickOrComponent = { title: "test", click: 54, component: 24, icon: "icon" } const errorWithBothWhenOneHasWrongType: ClickOrComponent = { title: "test", click: 54, component: "should be number here", icon: "icon" } type RequireOnlyOne = Pick> & { [K in Keys%5D-?: Required> & Partial, undefined>> }[Keys] type OnlyOneClickOrComponent = RequireOnlyOne const noErrorWithOnlyOne: OnlyOneClickOrComponent = { title: "test", click: 534, icon: "icon" } const errorWithBoth: OnlyOneClickOrComponent = { title: "test", click: 534, component: 53, icon: "icon" } //This interface will be used to trick OnlyOneClickAtComponent into allowing an object with both interface ClickMenuItem { title: string; click: number; icon: string; } const hasBoth = { title: "test", click: 54, component: 24, icon: "icon" } //No error because excess properties are only disallowed when directly assigning an object literal const temp: ClickMenuItem = hasBoth //No error despite temp actually having both because TS only knows that temp is a ClickMenuItem const trickIntoAllowingBoth: OnlyOneClickOrComponent = temp rel="nofollow noreferrer">Вот ссылка на игровую площадку TypeScript, показывающая оба в действии.

RequireOnlyOne с RequireOnlyOne заключается в том, что TypeScript не всегда знает во время компиляции каждое свойство, которое будет существовать во время выполнения. Очевидно, что RequireOnlyOne не может ничего сделать, чтобы предотвратить дополнительные свойства, о которых он не знает. Я привел пример того, как RequireOnlyOne может пропустить вещи в конце ссылки на игровую площадку.

Краткий обзор того, как это работает, на следующем примере:

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

type ClickOrComponent = RequireAtLeastOne<MenuItem, 'click' | 'component'>
  1. Pick<T, Exclude<keyof T, Keys>> из RequireAtLeastOne становится { title: string, icon: string}, которые являются неизменными свойствами ключей, не включенных в 'click' | 'component' 'click' | 'component'

  2. { [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>> }[Keys] из RequireAtLeastOne становится

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

    Который становится

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

    Который, наконец, становится

    {component: number, click?: number} | {click: number, component?: number}
    
  3. Пересечение шагов 1 и 2 выше

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

    упрощает до

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

Ответ 3

Альтернативой без нескольких интерфейсов является

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 passing a property that is not defined
  { title: "", icon: "", click: "", component: {} },
  // Does error out :)
  { title: "", icon: "" }
];

Я задал аналогичный вопрос о том, как создать Partial-like, для которого требуется установить одно свойство

Вышеупомянутое может быть упрощено, но его может быть или не быть легче читать

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

Обратите внимание, что ни одно из них не позволяет вам добавлять оба, потому что TypeScript разрешает дополнительные свойства объектов, которые используют AND/OR См. Https://github.com/Microsoft/TypeScript/issues/15447

Ответ 4

Я закончил тем, что сделал:

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

export interface MenuItemComponent extends MenuItem{
  component: any;
}

export interface MenuItemClick extends MenuItem{
  click: any;
}

Тогда я использовал:

 appMenuItems: Array<MenuItemComponent|MenuItemClick>;

Но надеялся, что найдется способ смоделировать его с помощью единого интерфейса.

Ответ 5

Мне нравится использовать Pick вместе с базовым типом, который включает в себя все свойства, чтобы установить эти виды условных требований.

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

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

Это чисто, а также гибко. Вы можете произвольно усложнить свои требования, утверждая, что "требуется либо все свойства, только эти два свойства, либо только это одно свойство" и т.д., Сохраняя при этом вашу декларацию простой и читаемой.

Ответ 6

Не могли бы вы ответить на этот вопрос, если бы не ответ? (Силианс "Хуан Мендес").

export type MenuItemCommon = {
  title: string;
  icon: string;
}

export type Component = {
    component: int;
}

export type Click = {
    click: string;
}

export type MenuItem = MenuItemCommon & (Component | Click)

my ide (intellij/webstorm) позволяет мне объявлять тип без ошибок, но жалуется, ссылаюсь ли я на компонент или клик. Я думаю, это правильно, потому что это не гарантировано. поэтому я должен ссылаться на него как

menuItem['component']

это, кажется, не мешает мне делать

menuItem:MenuItem = {title:"t",icon:"i",component:1,click:"c"}

т.е. указать оба свойства