Für die Typoskript-Schnittstelle muss eine von zwei Eigenschaften vorhanden sein


106

Ich versuche eine Schnittstelle zu erstellen, die haben könnte

export interface MenuItem {
  title: string;
  component?: any;
  click?: any;
  icon: string;
}
  1. Gibt es eine Möglichkeit zu fordern componentoder eingestellt clickzu werden
  2. Gibt es eine Möglichkeit zu verlangen, dass nicht beide Eigenschaften festgelegt werden können?

Antworten:


93

Nicht mit einer einzelnen Schnittstelle, da Typen keine bedingte Logik haben und nicht voneinander abhängen können, aber Sie können die Schnittstellen aufteilen:

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;

Interessant. Ich habe noch nie einen "Typ" exportiert. Es bedeutet also einfach, dass MenuItem das eine oder andere sein kann?
— Nix

Ich mag das besser als das, was ich unten gemacht habe. Link, der mir geholfen hat, "type" stackoverflow.com/a/36783051/256793
— Nix

Ja: TS hat ANDund OROperatoren für seine Typen. Wenn Sie so etwas tun, kann es hilfreich sein, wenn der OP Ihnen einen einzigen gesammelten Namen gibt, nachdem Sie eine Reihe einzelner Typen deklariert haben.
— Ssube

7
Hallo Leute, wie zwingst du dich, zwischen einer der Schnittstellen zu wählen, aber nicht zwischen beiden? Mit diesem Typ, ein Objekt mit beiden componentund wird click keinen Fehler machen ...
— Daniel Ramos

4
@DanielRamos können Sie hinzufügen click?: neverauf ComponentMenuItemund component?: neverauf ClickMenuItem.
— Ethan.roday

160

Mit Hilfe des Excludein TypeScript 2.8 hinzugefügten Typs wird eine verallgemeinerbare Möglichkeit bereitgestellt, mindestens eine von mehreren Eigenschaften zu benötigen:

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]

Und ein teilweiser, aber nicht absoluter Weg, um zu verlangen, dass nur einer bereitgestellt wird, ist:

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]

Hier ist ein TypeScript-Spielplatz-Link, der beide in Aktion zeigt.

Die Einschränkung RequireOnlyOnebesteht darin, dass TypeScript zur Kompilierungszeit nicht immer alle Eigenschaften kennt, die zur Laufzeit vorhanden sind. Es kann also offensichtlich RequireOnlyOnenichts unternommen werden, um zusätzliche Eigenschaften zu verhindern, von denen es nichts weiß. Ich habe RequireOnlyOneam Ende des Spielplatz-Links ein Beispiel gegeben, wie man Dinge übersehen kann.

Ein kurzer Überblick über die Funktionsweise anhand des folgenden Beispiels:

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

type ClickOrComponent = RequireAtLeastOne<MenuItem, 'click' | 'component'>
  1. Pick<T, Exclude<keyof T, Keys>>from RequireAtLeastOnewird { title: string, icon: string}, das sind die unveränderten Eigenschaften der Schlüssel, die nicht in enthalten sind'click' | 'component'

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

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

    Welches wird

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

    Welches wird schließlich

    {component: number, click?: number} | {click: number, component?: number}
    
  3. Der Schnittpunkt der Schritte 1 und 2 oben

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

    vereinfacht zu

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

6
Vielen Dank für beschreibende Beispiele. Wirklich informativ.
— Eduard

1
Vielen Dank für eine äußerst informative und wohlgeformte Antwort. Ich habe mit diesem Beispiel gespielt, damit ich es besser verstehen kann. Es scheint , dass , wenn Sie geben componentund clickeine Art andere dann anyein Objekt sowohl mit mindestens einem Satz von gültigen Eigenschaften passieren. Ich gehe davon aus, dass dies auf die Art und Weise zurückzuführen ist, in der sich der Typ auf { title: string, icon: string, component: any} | { title: string, icon: string, click: any}welche Zustände reduziert. Der Typ besteht aus 3 Eigenschaften anstelle von 4, wobei eine optional ist. Ich versuche, eine Dokumentation zur Verwendung des Arrays in der Kartennotation zu finden, kann dies aber nicht.
— Sam R

5
Kann jemand erklären, was der Bindestrich (Minuszeichen) im Ausdruck [K in keys]-bewirkt?
— Einseitig

2
@Lopsided Entfernt alle optionalen Modifikatoren, die möglicherweise Kim ursprünglichen Objekt vorhanden waren T. Siehe stackoverflow.com/questions/49655419/… Was den Zweck betrifft[K in Keys]-?: : Ich habe gerade einige Tests durchgeführt und es sieht so aus, als ob es keinen Unterschied im Endergebnis macht, aber ich habe es nur eingegeben, um sicherzugehen , dass es sich RequireAtLeastOneverhält auf die gleiche Weise, unabhängig davon, ob die angegebenen Eigenschaften Keysursprünglich optional waren oder nicht.
— KPD

2
Hinweis: Dies ist Teil des offiziellen Microsoft Azure Typescript SDK docs.microsoft.com/en-us/javascript/api/@azure/…
— Simon_Weaver

22

Es ist nicht erforderlich, komplexe bedingte Typen zu verwenden . Wir können es einfacher machen:

  1. Gibt es eine Möglichkeit, die Einstellung einer Komponente oder eines Klicks zu verlangen? (inklusive 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: {} } // ✔

Ein Vereinigungstyp ( |) entspricht inklusive OR. Es wird mit den nicht bedingten Eigenschaften geschnitten .

  1. Gibt es eine Möglichkeit zu verlangen, dass nicht beide Eigenschaften festgelegt werden können? (Exklusiv 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

Grundsätzlich sollte entweder component oder click kann eingestellt werden, das andere sollte niemals gleichzeitig hinzugefügt werden. TS kann einen diskriminierten Vereinigungstyp daraus machen MenuItemXor, was entspricht XOR.

Hinweis : Diese XORBedingung für MenuItemXorist bei akzeptierter Antwort nicht möglich .


Codebeispiel zum Herumspielen


2
wirklich reibungslose Antwort
— Ivan Caceres

1
Wie damit umzugehen testOr.click;, gibt den Fehler zurück Property 'click' does not exist on type 'MenuItemOr'.?
— Eduardo Dallmann

2
@EduardoDallmann Mit dem inOperator können Sie überprüfen, ob für das Objekt ( Spielplatz ) eine der beiden Eigenschaften vorhanden ist
— ford04

Gute Antwort! Aber können Sie erklären, wie es funktioniert? Es fällt mir schwer, die union ( |) mit nur einem richtigen Operanden zu verstehen . Vielen Dank
— NorTicUs

@NorTicUs Wenn Sie das führende |Innere & ( ... )von meinen MenuItemXor: Dies ist nur eine Verknüpfung für den Union-Operator zur besseren Formatierung / Trennung von Zeilen. Wie hier - keine Magie beteiligt :)
— ford04

13

Eine Alternative ohne mehrere Schnittstellen ist

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

Ich habe eine ähnliche Frage unter Erstellen eines Partial-like gestellt, für die eine einzelne Eigenschaft festgelegt werden muss

Das Obige könnte vereinfacht werden, aber es kann leichter zu lesen sein oder auch nicht

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

Beachten Sie, dass keines dieser Elemente Sie daran hindert, beide hinzuzufügen, da TypeScript zusätzliche Eigenschaften für Objekte zulässt, die AND / OR verwenden. Siehe https://github.com/Microsoft/TypeScript/issues/15447


1
Sie können &zu und extrahieren titleund iconzu einem separaten Typ. :)
— Robert Koritnik

5

Am Ende tat ich:

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

export interface MenuItemComponent extends MenuItem{
  component: any;
}

export interface MenuItemClick extends MenuItem{
  click: any;
}

Dann habe ich verwendet:

 appMenuItems: Array<MenuItemComponent|MenuItemClick>;

Ich hatte jedoch gehofft, dass es eine Möglichkeit gibt, es mit einer einzigen Schnittstelle zu modellieren.


1
Eigentlich schlägt Microsoft vor, es sollte sein:<MenuItemComponent|MenuItemClick>[]
— Loolooii

3

Ich verwende gerne Pickzusammen mit einem Basistyp, der alle Eigenschaften enthält, um diese Art von bedingten Anforderungen festzulegen.

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

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

Das ist sauber und auch flexibel. Sie können mit Ihren Anforderungen beliebig komplex werden, indem Sie Dinge wie "entweder alle Eigenschaften, nur diese beiden Eigenschaften oder nur diese eine Eigenschaft erfordern" usw. behaupten, während Sie Ihre Deklaration einfach und lesbar halten.


1
Schnittstellen können keinen Typ "=". Wenn dies export interface MenuItem = ...so wäre, wäre es gültig.
— J'e

2

Ich benutze das:

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

Verwendung:

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

Dies macht fieldAund fieldB erforderlich .


0

Um nur auf die coolen Antworten oben zu erweitern! Und für die Leute, die hier landen, während sie nach einer Teilversion suchen, die Fähigkeiten erfordert! Hier ein Ausschnitt, den ich gemacht habe!

PartialReq

Sie möchten einen Teil einer Schnittstelle haben, benötigen aber in der Zwischenzeit einige Felder! Hier, wie es gemacht wird

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

Verwenden Sie ein Beispiel

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

Hier können Sie sehen, dass es perfekt funktioniert

Geben Sie hier die Bildbeschreibung ein

Wenn das nicht zur Verfügung gestellt wird

Geben Sie hier die Bildbeschreibung ein

UPDATE: Für die Verwendung, die ich oben angedeutet habe, hier eine bessere Antwort

Ich ging einfach zum Arzt und fand Omit .

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

Ich bin gekommen, um es hinzuzufügen. Aber bevor ich es tue, habe ich gerade diese coole Antwort gesehen. Es deckt alles ab:

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

Probieren Sie es einfach aus! Es zeigt, wie es für die verschiedenen Versionen von Typescript geht! Und um nicht zu wiederholen! Geh und überprüfe!

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.