Pesquisa de site

Interface do Typescript - É possível tornar obrigatórias as propriedades 'uma ou outra'?


Possivelmente uma pergunta estranha, mas estou curioso se é possível criar uma interface onde uma propriedade ou outra seja obrigatória.

Então, por exemplo...

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

interface Attachment {...}

No caso acima, eu gostaria de garantir que ou text ou attachment exista.


É assim que estou fazendo agora. Achei um pouco verboso (digitando botkit para 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;
}

Resposta #1

Se você realmente quer "uma propriedade ou a outra" e não ambas, você pode usar never no tipo que está sendo estendido:

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

Exemplo no Playground

Resposta #2

Você pode usar um tipo união para fazer isso:

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

Se você quiser permitir tanto texto quanto anexo, você escreveria

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

Resposta #3

Você pode se aprofundar com a solução do @robstarbuck criando os seguintes tipos:

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

E então o tipo Message ficaria assim

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

Com esta solução você pode adicionar facilmente mais campos nos tipos MessageWithText ou MessageWithAttachment sem excluí-los em outro.

Resposta #4

Eu tropecei neste tópico enquanto procurava uma resposta para o meu caso (ou propA, propB ou nenhum deles). A resposta de Ryan Fujiwara quase resolveu, mas eu perdi algumas verificações por causa disso.

Minha solução:

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;

Esta solução mantém todas as verificações como deveriam ser. Talvez alguém ache isso útil :)

Resposta #5

Exemplo simples de 'preciso de um dos dois':

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

Resposta #6

Eu comprimi ainda mais a solução de @Voskanyan David para chegar a esta solução que, pessoalmente, acho muito elegante:

Você ainda precisa definir Only<> e Either<> em algum lugar

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>

Depois, torna-se possível defini-lo sem quaisquer interfaces/tipos intermediários como uma única coisa:

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

Artigos relacionados