TypeScript-Schnittstelle erfordert, dass eine von zwei Eigenschaften existiert
Ich versuche, eine Schnittstelle zu erstellen, die haben könnte
export interface MenuItem {
title: string;
component?: any;
click?: any;
icon: string;
}
- Gibt es eine Möglichkeit,
componentoderclickals erforderlich festzulegen - Gibt es eine Möglichkeit zu verlangen, dass beide Eigenschaften nicht gesetzt werden können?
Antwort #1
Mit Hilfe des Exclude-Typs, der in TypeScript 2.8 hinzugefügt wurde, wird eine verallgemeinerbare Methode bereitgestellt, um mindestens eine von einer Menge von Eigenschaften zu erfordern:
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 eine teilweise, aber nicht absolute Möglichkeit, zu verlangen, dass genau einer angegeben 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-Playground-Link, der beide in Aktion zeigt.
Die Einschränkung bei RequireOnlyOne besteht darin, dass TypeScript zur Kompilierungszeit nicht immer weiß, welche Eigenschaften zur Laufzeit existieren werden. Daher kann RequireOnlyOne natürlich nichts tun, um zusätzliche Eigenschaften zu verhindern, von denen es nichts weiß. Ich habe am Ende des Playground-Links ein Beispiel gezeigt, wie RequireOnlyOne Dinge übersehen kann.
Ein kurzer Überblick darüber, wie es funktioniert, anhand des folgenden Beispiels:
interface MenuItem {
title: string;
component?: number;
click?: number;
icon: string;
}
type ClickOrComponent = RequireAtLeastOne<MenuItem, 'click' | 'component'>
Pick<T, Exclude<keyof T, Keys>>vonRequireAtLeastOnewird zu{ title: string, icon: string}, was die unveränderten Eigenschaften der Schlüssel sind, die nicht in'click' | 'component'enthalten sind{ [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>> }[Keys]vonRequireAtLeastOnewird zu{ component: Required<{ component?: number }> & { click?: number }, click: Required<{ click?: number }> & { component?: number } }[Keys]Was wird
{ component: { component: number, click?: number }, click: { click: number, component?: number } }['component' | 'click']Welche schließlich wird
{component: number, click?: number} | {click: number, component?: number}Die Schnittmenge von Schritt 1 und Schritt 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}
Antwort #2
Nicht mit einer einzigen Schnittstelle, da Typen keine bedingte Logik haben und nicht voneinander abhängen können, aber Sie können dies tun, indem Sie 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;
Antwort #3
Es gibt eine einfachere Lösung. Es ist nicht nötig, sich auf any oder komplexe bedingte Typen (siehe Antwort) zu verlassen:
- Gibt es eine Möglichkeit, dass entweder Component oder Click festgelegt werden müssen? (Inklusive
ODER)
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 einem inklusiven ODER. Er wird mit den nicht-wechselnden Eigenschaften geschnitten.
Verwenden Sie den in-Operator, um den Wert wieder auf einen der Bestandteile einzugrenzen:
if ("click" in testOr) testOr.click // works
- Gibt es eine Möglichkeit, zu erzwingen, dass beide Eigenschaften nicht gesetzt werden können? (Exklusives
ODER/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 set
Im Grunde kann entweder component oder click festgelegt werden, das andere sollte niemals 1 gleichzeitig hinzugefügt werden. TS kann einen diskriminierten Vereinigungs-Typ aus MenuItemXor erstellen, der XOR entspricht.
Diese XOR-Bedingung für MenuItemXor ist mit der akzeptierten Antwort nicht möglich.
Spielplatz
1 Technisch gesehen wird prop?: never zu prop?: undefined aufgelöst, obwohl Erstere oft zur Veranschaulichung verwendet wird.
Antwort #4
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 gestellt bei Wie man ein Partial-Ähnliches erstellt, das verlangt, dass eine einzelne Eigenschaft gesetzt wird
Das Obige könnte vereinfacht werden, aber es könnte leichter oder auch nicht leichter zu lesen sein.
export type MenuItem = {
title: string;
icon: string;
} & (
{component: any} | {click: string}
)
Beachten Sie, dass keines davon Sie daran hindert, beides hinzuzufügen, da TypeScript zusätzliche Eigenschaften bei Objekten erlaubt, die UND/ODER verwenden. Siehe https://github.com/Microsoft/TypeScript/issues/15447
Antwort #5
Ich benutze dies:
type RequireField<T, K extends keyof T> = T & Required<Pick<T, K>>
Verwendung :
let a : RequireField<TypeA, "fieldA" | "fieldB">;
Dies macht fieldA und fieldB erforderlich.
