Rozhranie TypeScript - Je možné, aby boli vlastnosti „jedna alebo druhá“ povinné?


Možno zvláštna otázka, ale zaujímalo by ma, či je možné vytvoriť rozhranie, kde je vyžadovaná buď jedna vlastnosť, alebo druhá.

Takže, napríklad...

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

interface Attachment {...}

V uvedenom prípade by som chcel zabezpečiť, aby existoval buď text, alebo attachment.


Takto to robím práve teraz. Myslel som, že je to trochu rozvláčne (píšem botkit pre 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;
}

Odpoveď č. 1

Ak naozaj chcete „jednu vlastnosť alebo inú“ a nie obe, môžete v rozširujúcom type použiť never:

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'}

Príklad na ihrisku

Odpoveď č. 2

Na to môžete použiť typ zjednotenia:

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

Ak chcete povoliť text aj prílohu, napísali by ste

type Message = MessageWithText | MessageWithAttachment | (MessageWithText & MessageWithAttachment);

Odpoveď č. 3

Môžete ísť hlbšie s riešením @robstarbuck vytvorením nasledujúcich typov:

type Only<T, U> = {
  [P in keyof T]: T[P];
} & {
  [P in keyof U]?: never;
};

type Either<T, U> = Only<T, U> | Only<U, T>;

A potom by typ Message vyzeral takto

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

S týmto riešením môžete ľahko pridať viac polí v typoch MessageWithText alebo MessageWithAttachment bez toho, aby ste ich vylúčili v inom.

Odpoveď č. 4

Na tento príspevok som narazil, keď som hľadal odpoveď pre svoj prípad (buď propA, propB alebo ani jeden z nich). Odpoveď od Ryana Fujiwaru bola takmer správna, ale tým som stratil niektoré kontroly.

Moje riešenie:

interface Base {   
  baseProp: string; 
}

interface ComponentWithPropA extends Base {
  propA: string;
  propB?: never;
}

interface ComponentWithPropB extends Base {
  propB: string;
  propA?: never;
} 

interface ComponentWithoutProps extends Base {
  propA?: never;
  propB?: never;
}

type ComponentProps = ComponentWithPropA | ComponentWithPropB | ComponentWithoutProps;

Toto riešenie zachováva všetky kontroly tak, ako majú byť. Možno niekto to nájde užitočné :)

Odpoveď č. 5

Jednoduchý príklad 'potrebujete jeden z dvoch':

type Props =
  | { factor: Factor; ratings?: never }
  | { ratings: Rating[]; factor?: never }

Odpoveď č. 6

Ďalej som skondenzoval riešenie od @Voskanyan David, aby som sa dostal k tomuto riešeniu, ktoré osobne považujem za veľmi pekné:

Stále musíte niekde definovať Only<> a Either<>

type Only<T, U> = {
    [P in keyof T]: T[P];
} & {
    [P in keyof U]?: never;
};

type Either<T, U> = Only<T, U> | Only<U, T>

Následne je možné ho definovať bez akýchkoľvek medzistupňových rozhraní/typov ako jednu vec:

type Message = {
    type?: string;
    channel?: string;
    user?: string;
    // ...etc
} & Either<{message: string}, {attachment: Attachment}>

Súvisiace články