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