Interfejs maszynopisu wymaga istnienia jednej z dwóch właściwości


112

Próbuję stworzyć interfejs, który mógłby mieć

export interface MenuItem {
  title: string;
  component?: any;
  click?: any;
  icon: string;
}
  1. Czy istnieje sposób, aby wymagać componentlub clickzostać ustawione
  2. Czy istnieje sposób, aby wymagać, aby nie można było ustawić obu właściwości?

Odpowiedzi:


103

Nie z pojedynczym interfejsem, ponieważ typy nie mają logiki warunkowej i nie mogą od siebie zależeć, ale możesz podzielić interfejsy:

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;

Ciekawy. Nigdy wcześniej nie eksportowałem „typu”. Więc to po prostu oznacza, że ​​MenuItem może być jednym lub drugim?
— Nix

Podoba mi się to bardziej niż to, które zrobiłem poniżej. Link, który pomógł mi zrozumieć „wpisz” stackoverflow.com/a/36783051/256793
— Nix

7
Cześć wszystkim, jak wymuszacie wybór między jednym z interfejsów, ale nie obydwoma? W przypadku tego typu obiekt z obydwoma componenti click nie popełni błędu ...
— Daniel Ramos,

5
@DanielRamos można dodać click?: neverna ComponentMenuItemi component?: neverna ClickMenuItem.
— ethan.roday

1
Czy jest jakiś sposób, aby to było zgodne z destrukturyzacją parametrów? Otrzymuję błędy TS, jeśli próbuję function myFunc({ title, icon, component, click }: MenuItem) „składnik” właściwości nie istnieje w typie „MenuItem”. „Kliknięcie” właściwości nie istnieje w typie „MenuItem”.
— CletusW

164

Przy pomocy Excludetypu, który został dodany w TypeScript 2.8, można uogólnić sposób wymagania co najmniej jednej z zestawu właściwości:

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]

Częściowym, ale nie absolutnym sposobem wymagania, aby jeden i tylko jeden był zapewniony, jest:

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]

Oto link do placu zabaw TypeScript pokazujący oba w akcji.

Zastrzeżenie RequireOnlyOnepolega na tym, że TypeScript nie zawsze zna w czasie kompilacji każdą właściwość, która będzie istniała w czasie wykonywania. Więc oczywiście RequireOnlyOnenie można nic zrobić, aby zapobiec dodatkowym właściwościom, o których nie wie. Podałem przykład tego, jak RequireOnlyOnemożna przeoczyć rzeczy na końcu linku do placu zabaw.

Krótkie omówienie tego, jak to działa na poniższym przykładzie:

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

type ClickOrComponent = RequireAtLeastOne<MenuItem, 'click' | 'component'>
  1. Pick<T, Exclude<keyof T, Keys>>from RequireAtLeastOnestaje się { title: string, icon: string}, czyli niezmienionymi właściwościami kluczy nieuwzględnionych w'click' | 'component'

  2. { [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>> }[Keys]od RequireAtLeastOnestaje

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

    Który staje się

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

    Który w końcu się staje

    {component: number, click?: number} | {click: number, component?: number}
    
  3. Punkt przecięcia kroków 1 i 2 powyżej

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

    upraszcza do

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

6
Bardzo dziękuję za opisowe przykłady. Naprawdę pouczające.
— Eduard,

1
Dzięki za niezwykle pouczającą i dobrze sformułowaną odpowiedź. Bawiłem się tym przykładem, aby lepiej go zrozumieć. Wygląda na to, że jeśli podasz componenti clicktyp inny, to anyprzejdzie obiekt z przynajmniej jednym zestawem prawidłowych właściwości. Zakładam, że wynika to ze sposobu, w jaki typ redukuje się do { title: string, icon: string, component: any} | { title: string, icon: string, click: any}Które stany typ to 3 właściwości zamiast 4, z których jedna jest opcjonalna. Próbuję znaleźć dokumentację dotyczącą użycia tablicy w notacji mapy, ale nie mogę.
— Sam R

5
Czy ktoś może wyjaśnić, co robi łącznik (znak minus) w wyrażeniu [K in keys]-?
— Krzywy

2
@Lopsided Usuwa wszelkie opcjonalne modyfikatory, które mogły istnieć Kw oryginalnym obiekcie T. Zobacz stackoverflow.com/questions/49655419/… Jeśli chodzi o jego cel, w [K in Keys]-?:szczególności: Właśnie przeprowadziłem kilka testów i wygląda na to, że faktycznie nie ma to wpływu na ostateczny wynik, ale wstawiłem go, aby mieć pewność, że RequireAtLeastOnezachowuje się w ten sam sposób, niezależnie od tego, czy właściwości określone dla Keysbyły pierwotnie opcjonalne, czy nie.
— KPD

2
Uwaga: jest to część oficjalnego zestawu Microsoft Azure Typescript
— Simon_Weaver

29

Nie ma potrzeby używania złożonych typów warunkowych . Możemy to uprościć:

  1. Czy istnieje sposób, aby ustawić wymaganie komponentu lub kliknięcie? (włącznie 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: {} } // ✔

Union Typ ( |) odpowiada włącznie OR. Jest przecięte z właściwościami non-warunkowych.

  1. Czy istnieje sposób, aby wymagać, aby nie można było ustawić obu właściwości? (Wyłącznie 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

Zasadniczo jedno component lub drugieclick można ustawić, inne nigdy nie powinny być dodawane w tym samym czasie. TS może wyodrębnić z unii dyskryminowany typMenuItemXor , który odpowiada XOR.

Uwaga : ten XORwarunek MenuItemXornie jest możliwy przy zaakceptowanej odpowiedzi .


Przykładowy kod do zabawy


2
naprawdę gładka odpowiedź
— Ivan Caceres

3
Jak sobie z testOr.click;tym poradzić, że zwraca błąd Property 'click' does not exist on type 'MenuItemOr'.?
— Eduardo Dallmann

2
@EduardoDallmann możesz użyć inoperatora, aby sprawdzić, czy na obiekcie istnieją właściwości ( plac zabaw )
— ford04

Niezła odpowiedź! Ale czy możesz wyjaśnić, jak to działa? Mam trudności ze zrozumieniem unii ( |) tylko z właściwym operandem. Dziękuję
— NorTicUs

@NorTicUs Jeśli masz na myśli wiodącej |wnętrze & ( ... )z MenuItemXor: jest to po prostu skrót dla operatora Unia dla lepszego rodzaju formatowania / oddzielającej nad liniami. Jak tutaj - bez magii :)
— ford04

14

Alternatywą bez wielu interfejsów jest

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

Zadałem podobne pytanie w temacie Jak utworzyć element podobny do częściowego, który wymaga ustawienia jednej właściwości

Powyższe można uprościć, ale może być łatwiejsze do odczytania lub nie

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

Zauważ, że żadna z nich nie uniemożliwia dodania obu, ponieważ TypeScript zezwala na dodatkowe właściwości obiektów używających AND / OR Zobacz https://github.com/Microsoft/TypeScript/issues/15447


1
Możesz użyć &do i wyodrębnić titlei icondo oddzielnego typu. :)
— Robert Koritnik

5

Skończyło się na zrobieniu:

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

export interface MenuItemComponent extends MenuItem{
  component: any;
}

export interface MenuItemClick extends MenuItem{
  click: any;
}

Wtedy użyłem:

 appMenuItems: Array<MenuItemComponent|MenuItemClick>;

Miałem jednak nadzieję, że istnieje sposób na modelowanie go za pomocą jednego interfejsu.


1
Właściwie Microsoft sugeruje, że powinno to być:<MenuItemComponent|MenuItemClick>[]
— Loolooii,

3

Lubię używać Pickrazem z typem podstawowym, który zawiera wszystkie właściwości, aby ustalić tego rodzaju wymagania warunkowe.

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

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

To jest czyste, a także elastyczne. Możesz dowolnie skomplikować swoje wymagania, twierdząc, że „wymagaj wszystkich właściwości, tylko tych dwóch lub tylko tej jednej właściwości” i tak dalej, zachowując prostą i czytelną deklarację.


1
interfejsy nie mogą "=" typu. Gdyby to było export interface MenuItem = ..., byłoby ważne.
— J'e

2

Używam tego:

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

Stosowanie:

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

To sprawia, że fieldAi fieldB wymagane .


0

Aby tylko rozszerzyć powyższe fajne odpowiedzi! I dla ludzi, którzy lądują tutaj w poszukiwaniu częściowej wersji z wymagającymi możliwościami! Oto fragment, który zrobiłem!

PartialReq

Chcesz mieć część interfejsu, ale w międzyczasie potrzebujesz niektórych pól! Oto jak to się robi

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

Użyj przykładu

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

Tutaj widać, że działa idealnie

wprowadź opis obrazu tutaj

Jeśli wymagane nie podano

wprowadź opis obrazu tutaj

AKTUALIZACJA: W przypadku użycia, który zasugerowałem powyżej, lepsza odpowiedź

Właśnie poszedłem do lekarza i znalazłem Omit .

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

Przyszedłem to dodać. Ale zanim to zrobię, właśnie zobaczyłem tę fajną odpowiedź. Obejmuje wszystko:

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

Po prostu to sprawdź! Pokazuje, jak to zrobić dla wszystkich różnych wersji Typescript! I żeby się nie powtarzać! Idź i sprawdź!

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.