Discriminated Unions: como lidar com diferentes types no TypeScript
No meu projeto mais recente, uma aplicação no estilo AMA (Ask Me Anything) feita com React e Golang, teve uma funcionalidade que me ajudou muito na hora de lidar com as diferentes propriedades que foram surgindo ao longo do desenvolvimento.
Por seguir o esquema de perguntas e respostas, muitos dos tipos de webhooks do app seriam referentes às mensagens e ao engajamento dos usuários com elas, ja que a ideia é que a comunidade possa votar nas suas principais dúvidas em tempo real.
Para essa dinâmica, vamos ter as variações de mensagem: criada; respondida; com mais votos; com menos votos.
Aqui nesse artigo vou trazer alguns exemplos de como o discriminated unions foi um diferencial no desenvolvimento dessa etapa do projeto que criei exclusivamente para o evento Go+React na Prática.
O que são Discriminated Unions?
Discriminated unions, ou tagged unions, é uma funcionalidade poderosa do TypeScript que permite definir tipos que podem assumir várias formas distintas. Cada forma é identificada por uma propriedade discriminante comum, facilitando a manipulação segura e intuitiva dos diferentes tipos de dados.
Isso é particularmente útil para modelar dados com múltiplas variantes, uma vez que essa definição garante que todas sejam tratadas explicitamente no código.
No exemplo do projeto AMA,
WebhookMessage
é um discriminated union com quatro variantes: message_created
, message_answered
, message_reaction_increased
e message_reaction_decreased
.Cada variante é identificada pela propriedade
kind
, permitindo o tratamento seguro e eficiente dos diferentes tipos de mensagens.type WebhookMessage = | { kind: "message_created"; value: { id: string, message: string } } | { kind: "message_answered"; value: { id: string } } | { kind: "message_reaction_increased"; value: { id: string; count: number } } | { kind: "message_reaction_decreased"; value: { id: string; count: number } };
A função
useMessagesWebSockets
ilustra como usar discriminated unions para processar mensagens recebidas via WebSocket. A propriedade kind
é usada para determinar o tipo da mensagem e aplicar a lógica apropriada:export function useMessagesWebSockets({ roomId }: useMessagesWebSocketsParams) { useEffect(() => { const ws = new WebSocket(`ws://localhost:8080/subscribe/${roomId}`); ws.onmessage = (event) => { const data: WebhookMessage = JSON.parse(event.data); switch(data.kind) { case 'message_created': // Ação quando uma nova mensagem é criada case 'message_answered': // Ação quando uma mensagem é respondida case 'message_reaction_increased': case 'message_reaction_decreased': // Ação quando uma mensagem recebe/perde curtidas } } return () => ws.close(); }, [roomId]); }
Somando à funcionalidade do discriminated unions, podemos usar o Zod para melhorar a validação e parsing de dados (extração das informações necessárias de algum dado) dessa maneira:
const webhookMessage = z.discriminatedUnion('kind', [ z.object({ kind: z.literal('message_created'), value: z.object({ id: z.string().uuid(), room_id: z.string().uuid(), message: z.string(), }), }), z.object({ kind: z.literal('message_answered'), value: z.object({ id: z.string().uuid(), }), }), z.object({ kind: z.literal('message_reaction_increased'), value: z.object({ id: z.string().uuid(), count: z.number(), }), }), z.object({ kind: z.literal('message_reaction_decreased'), value: z.object({ id: z.string().uuid(), count: z.number(), }), }), ]) export type WebhookMessage = z.infer<typeof webhookMessage> const message = webhookMessage.parse(JSON.parse(event.data));
Esta abordagem não só torna o código mais claro e fácil de entender, mas também garante que todas as variantes possíveis de
WebhookMessage
sejam tratadas explicitamente, evitando erros e melhorando a manutenibilidade e a confiabilidade do código.