Typoskript-Schnittstelle - Ist es möglich, die eine oder andere Eigenschaft erforderlich zu machen?

75

Möglicherweise eine seltsame Frage, aber ich bin gespannt, ob es möglich ist, eine Schnittstelle zu erstellen, bei der die eine oder andere Eigenschaft erforderlich ist.

Also zum Beispiel ...

interface Message {
    text: string;
    attachment: Attachment;
    timestamp?: number;
    // ...etc
}

interface Attachment {...}

Im obigen Fall möchte ich sicherstellen, dass entweder textoder attachmentvorhanden ist.

Hoffentlich macht das Sinn.

Danke im Voraus!


Bearbeiten: So mache ich es gerade. Dachte, es war ein bisschen ausführlich (Eingabe von Botkit für Slack).

interface Message {
    type?: string;
    channel?: string;
    user?: string;
    text?: string;
    attachments?: Slack.Attachment[];
    ts?: string;
    team?: string;
    event?: string;
    match?: [string, {index: number}, {input: string}];
}

interface AttachmentMessageNoContext extends Message {
    channel: string;
    attachments: Slack.Attachment[];
}

interface TextMessageNoContext extends Message {
    channel: string;
    text: string;
}
Dsifford
quelle
Schauen Sie sich hier meinen Lösungsvorschlag an .
ford04

Antworten:

80

Sie können dazu einen Unionstyp verwenden:

interface MessageBasics {
  timestamp?: number;
  /* more general properties here */
}
interface MessageWithText extends MessageBasics {
  text: string;
}
interface MessageWithAttachment extends MessageBasics {
  attachment: Attachment;
}
type Message = MessageWithText | MessageWithAttachment;

Wenn Sie sowohl Text als auch Anhang zulassen möchten, würden Sie schreiben

type Message = MessageWithText | MessageWithAttachment | (MessageWithText & MessageWithAttachment);
Ryan Cavanaugh
quelle
5
Dies erlaubt ihm jedoch nicht, eine Nachricht mit einem Text und einem Anhang zu erhalten
Roberto
1
Danke für die Antwort Ryan! So mache ich es gerade. Ich habe das zu meiner Frage oben hinzugefügt. Ich war mir nicht sicher, ob es einen saubereren Weg gab.
Dsifford
6
@ Roberto Aber das oder nicht ausschließen! Dies akzeptiert beide.
Alessandro_russo
5
Die Antwort funktioniert nicht wie beabsichtigt, Typ Messageakzeptiert {text: 'a', attachment: 'b'} typescriptlang.org/play/...
golopot
1
Das letzte `| (MessageWithText & MessageWithAttachment) `macht nichts - es verhält sich ohne es genauso.
mik01aj
37

Wenn Sie wirklich nach "der einen oder der anderen Eigenschaft" suchen und nicht nach beiden, können Sie neverim Erweiterungstyp Folgendes verwenden:

interface MessageBasics {
  timestamp?: number;
  /* more general properties here */
}
interface MessageWithText extends MessageBasics {
  text: string;
  attachment?: never;
}
interface MessageWithAttachment extends MessageBasics {
  text?: never;
  attachment: string;
}
type Message = MessageWithText | MessageWithAttachment;

// 👍 OK 
let foo: Message = {attachment: 'a'}

// 👍 OK
let bar: Message = {text: 'b'}

// ❌ ERROR: Type '{ attachment: string; text: string; }' is not assignable to type 'Message'.
let baz: Message = {attachment: 'a', text: 'b'}

Beispiel auf dem Spielplatz

Robstarbuck
quelle
3
@ Golopot, nachdem Sie Ihren Kommentar gelesen haben, sollte dies die Lösung sein, nach der Sie gesucht haben.
Robstarbuck
3
Dies ist die beste, sie schreit, wenn die 2 Eigenschaften
zugewiesen werden
Danke dafür. Dieses Beispiel hat mir geholfen :)
Yuschick
1
Diese Antwort zeigt, wie dieser "XOR" -Typ automatisiert wird: stackoverflow.com/a/61350902/2441655
Venryx
6

Danke @ ryan-cavanaugh, der mich in die richtige Richtung gebracht hat.

Ich habe einen ähnlichen Fall, aber dann mit Array-Typen. Ich habe ein bisschen mit der Syntax zu kämpfen, deshalb habe ich sie hier zur späteren Bezugnahme eingefügt:

interface BaseRule {
  optionalProp?: number
}

interface RuleA extends BaseRule {
  requiredPropA: string
}

interface RuleB extends BaseRule {
  requiredPropB: string
}

type SpecialRules = Array<RuleA | RuleB>

// or

type SpecialRules = (RuleA | RuleB)[]

// or (in the strict linted project I'm in):

type SpecialRule = RuleA | RuleB
type SpecialRules = SpecialRule[]

Aktualisieren:

Beachten Sie, dass Sie später möglicherweise immer noch Warnungen erhalten, wenn Sie die deklarierte Variable in Ihrem Code verwenden. Sie können dann die (variable as type)Syntax verwenden. Beispiel:

const myRules: SpecialRules = [
  {
    optionalProp: 123,
    requiredPropA: 'This object is of type RuleA'
  },
  {
    requiredPropB: 'This object is of type RuleB'
  }
]

myRules.map((rule) => {
  if ((rule as RuleA).requiredPropA) {
    // do stuff
  } else {
    // do other stuff
  }
})
publicJorn
quelle
9
Nur eine kurze Notiz, um Sie wissen zu lassen, dass es eine typsichere, intelligente Methode zum Schreiben Ihres .map()Codes gibt. Verwenden Sie beim inBediener den Typschutz. TypeScript führt die richtige Typinferenz usw. durch. Ihr Zustand wäre alsoif('requiredPropA' in rule) { /* inside here, TS knows rule is of type <RuleA> */ }
Eric Liprandi
2

Sie können einige Schnittstellen für die erforderlichen Bedingungen erstellen und sie in einem Typ wie hier verbinden:

interface SolidPart {
    name: string;
    surname: string;
    action: 'add' | 'edit' | 'delete';
    id?: number;
}
interface WithId {
    action: 'edit' | 'delete';
    id: number;
}
interface WithoutId {
    action: 'add';
    id?: number;
}

export type Entity = SolidPart & (WithId | WithoutId);

const item: Entity = { // valid
    name: 'John',
    surname: 'Doe',
    action: 'add'
}
const item: Entity = { // not valid, id required for action === 'edit'
    name: 'John',
    surname: 'Doe',
    action: 'edit'
}
cuddlemeister
quelle
2

Ok, also nach einigem Ausprobieren und Googeln stellte ich fest, dass die Antwort für meinen Anwendungsfall nicht wie erwartet funktionierte. Für den Fall, dass jemand anderes das gleiche Problem hat, dachte ich, ich würde mitteilen, wie ich es zum Laufen gebracht habe. Meine Schnittstelle war so:

export interface MainProps {
  prop1?: string;
  prop2?: string;
  prop3: string;
}

Was ich suchte, war eine Typdefinition, die besagen würde, dass wir weder prop1 noch prop2 definieren könnten. Wir hätten prop1 definieren können, aber nicht prop2. Und schließlich habe prop2 definiert, aber nicht prop1. Hier ist, was ich als Lösung gefunden habe.

interface MainBase {
  prop3: string;
}

interface MainWithProp1 {
  prop1: string;
}

interface MainWithProp2 {
  prop2: string;
}

export type MainProps = MainBase | (MainBase & MainWithProp1) | (MainBase & MainWithProp2);

Dies funktionierte perfekt, mit der Ausnahme, dass beim Versuch, entweder auf prop1 oder prop2 in einer anderen Datei zu verweisen, immer wieder eine Eigenschaft abgerufen wurde, kein TS-Fehler vorhanden ist. So konnte ich das umgehen:

import {MainProps} from 'location/MainProps';

const namedFunction = (props: MainProps) => {
    if('prop1' in props){
      doSomethingWith(props.prop1);
    } else if ('prop2' in props){
      doSomethingWith(props.prop2);
    } else {
      // neither prop1 nor prop2 are defined
    }
 }

Ich dachte nur, ich würde das teilen, denn wenn ich auf dieses bisschen Verrücktheit stoßen würde, wäre es wahrscheinlich auch jemand anderes.

Ryan Fujiwara
quelle
1

Es gibt einige coole Typescript-Optionen, die Sie unter https://www.typescriptlang.org/docs/handbook/utility-types.html#omittk verwenden können

Ihre Frage lautet: Erstellen Sie eine Schnittstelle, in der entweder "Text" oder ein Anhang vorhanden ist. Sie könnten so etwas tun wie:

interface AllMessageProperties {
  text: string,
  attachement: string,
}

type Message = Omit<AllMessageProperties, 'text'> | Omit<AllMessageProperties, 'attachement'>;

const messageWithText : Message = {
  text: 'some text'
}

const messageWithAttachement : Message = {
  attachement: 'path-to/attachment'
}

const messageWithTextAndAttachement : Message = {
  text: 'some text',
  attachement: 'path-to/attachment'
}

// results in Typescript error
const messageWithOutTextOrAttachement : Message = {

}
Stefan van de Vooren
quelle
Gibt es eine Möglichkeit, dieses textXOR zu attachmentverwenden Omit?
Chris R