інтерфейс машинопису вимагає існування одного з двох властивостей


112

Я намагаюся створити інтерфейс, який міг би бути

export interface MenuItem {
  title: string;
  component?: any;
  click?: any;
  icon: string;
}
  1. Чи є спосіб вимагати componentабо clickвстановлювати
  2. Чи є спосіб вимагати, щоб обидві властивості не могли бути встановлені?

Відповіді:


103

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

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;

Цікаво. Я ніколи раніше не експортував "тип". Тож це просто означає, що MenuItem може бути як одним, так і іншим?
— Нікс

Мені це подобається більше, ніж тому, що мені сподобалось нижче. Посилання, яке допомогло мені зрозуміти "тип" stackoverflow.com/a/36783051/256793
— Nix,

7
Привіт, хлопці, як змусити обирати один з інтерфейсів, але не обидва? При цьому типі об’єкт з обома componentі click не помиляється ...
— Даніель Рамос,

5
@DanielRamos ви можете додати click?: neverна ComponentMenuItemі component?: neverна ClickMenuItem.
— ethan. День народження

1
Будь-який спосіб зробити це сумісним із деструктуризацією параметрів? Я отримую помилки TS, якщо спробую function myFunc({ title, icon, component, click }: MenuItem) властивість 'компонент' не існує у типі 'MenuItem'. Властивість 'click' не існує для типу 'MenuItem'.
— CletusW

164

За допомогою 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]

Ось посилання на ігровий майданчик TypeScript, де показано обидва в дії.

Застереження 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'

  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}
    

6
Щиро дякую за описові приклади. Дійсно інформативно.
— Едуард

1
Дякуємо за надзвичайно інформативну та добре сформовану відповідь. Я грав із цим прикладом, щоб краще зрозуміти його. Схоже, що якщо ви даєте componentі clickтип інший, то anyпройде об'єкт, що має принаймні один набір допустимих властивостей. Я припускаю, що це через те, як тип зводиться до { title: string, icon: string, component: any} | { title: string, icon: string, click: any}Яких станів тип - це 3 властивості замість 4, причому одне є необов’язковим. Я намагаюся знайти документацію щодо використання масиву в позначенні карти, але не можу.
— Sam R

5
Хтось може пояснити, що робить дефіс (знак мінус) у виразі [K in keys]-?
— однобокий

2
@Lopsided Видаляє будь-які необов’язкові модифікатори, які могли існувати Kв оригінальному об’єкті T. Див stackoverflow.com/questions/49655419 / ... Що стосується його цілей в [K in Keys]-?:зокрема: Я просто зробив деякі випробування , і це виглядає , як він на самому ділі не робить різниці в кінцевому результаті, але я поклав його в просто щоб бути впевненим , що RequireAtLeastOneповодиться так само незалежно від того, чи були властивості, вказані для Keys, спочатку необов’язковими чи ні.
— КПД,

2
Примітка: Це частина офіційного пакета
— Simon_Weaver

29

Немає необхідності використовувати складні умовні типи . Ми можемо зробити це простіше:

  1. Чи є спосіб вимагати встановлення компонента або клацання? (включно 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: {} } // ✔

Типу союз ( |) відповідає включно OR. Він перетинається з безумовними властивостями.

  1. Чи є спосіб вимагати, щоб обидві властивості не могли бути встановлені? (Ексклюзивно 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 are set

В основному або component або click може бути встановлений, то інший не повинен ніколи бути додані одночасно. TS може створити дискримінаційний тип об'єднання MenuItemXor, який відповідаєXOR .

Примітка : Ця XORумова для MenuItemXorнеможлива з прийнятою відповіддю .


Зразок коду, з яким можна пограти


2
дійсно гладка відповідь
— Іван Касерес

3
Як бути з цим, testOr.click;що повертає помилку Property 'click' does not exist on type 'MenuItemOr'.?
— Едуардо Даллманн,

2
@EduardoDallmann, за допомогою inоператора можна перевірити, чи є на об’єкті будь-які властивості ( ігровий майданчик )
— ford04

Приємна відповідь! Але чи можете ви пояснити, як це працює? Мені важко зрозуміти union ( |) лише з правильним операндом. Дякую
— NorTicUs

@NorTicUs Якщо ви маєте на увазі провідну |внутрішню & ( ... )частину MenuItemXor: це просто ярлик для оператора об'єднання для кращого форматування / розділення рядків. Як тут - без участі магії :)
— ford04

14

Альтернативою без декількох інтерфейсів є

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: "" }
];

Я задав подібне запитання в розділі Як створити подібний до часткового, який вимагає встановлення однієї властивості

Вищезазначене можна спростити, але це може бути чи не бути легшим для читання

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

Зверніть увагу, що жодне з них не заважає вам додавати обидва, оскільки TypeScript дійсно дозволяє додаткові властивості об’єктів, що використовують І / АБО. Див. Https://github.com/Microsoft/TypeScript/issues/15447


1
Ви можете використовувати &і для вилучення, titleі iconдля окремого типу. :)
— Роберт Коритник,

5

У підсумку я зробив:

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

Але сподівався, що є спосіб змоделювати його за допомогою єдиного інтерфейсу.


1
Насправді Microsoft пропонує, щоб це було:<MenuItemComponent|MenuItemClick>[]
— Loolooii

3

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

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

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

Це чисто, а також гнучко. Ви можете довільно ускладнювати свої вимоги, стверджуючи такі речі, як "вимагати або всіх властивостей, лише ці дві властивості, або лише цю одну властивість" тощо, зберігаючи декларацію простою та читабельною.


1
інтерфейси не можуть "=" типу. Якби це було, export interface MenuItem = ...це було б дійсним.
— J'e

2

Я використовую це:

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

Використання:

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

Це робить fieldAі fieldB потрібно .


0

Просто поширюється на круті відповіді вище! І для людей, які приземляються тут, шукаючи Часткову версію з необхідними можливостями! Ось фрагмент, який я зробив!

PartialReq

Ви хочете мати частковий інтерфейс, але тим часом вимагаєте деяких полів! Ось як це робиться

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

Використовуйте приклад

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
    })
);

Тут ви бачите, що це працює ідеально

введіть тут опис зображення

Якщо необхідне не передбачено

введіть тут опис зображення

ОНОВЛЕННЯ: Для використання, яке я мав на увазі вище, краща відповідь

Я просто пройшов за документом і знайшов Оміта .

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

Я прийшов додати його. Але перед тим, як це зробити, я просто побачив цю класну відповідь. Це охоплює все:

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

Просто перевірте! Він показує, як це зробити для всіх різних версій Typescript! І заради того, щоб не повторювати! Йди і перевір!

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.