Os tipos de união são úteis quando você tem uma coleção de classes que não agem entre si, mas que devem ser processadas da mesma maneira.
Como desenvolvedor de software na Swimlane, às vezes tenho a oportunidade de aprender coisas novas que beneficiam não só a mim e ao resto da equipe, mas também tornam nossos produtos mais eficazes. Em um projeto recente em que trabalhei, fui encarregado de adicionar um Log de Auditoria. O requisito era registrar cada alteração (por exemplo, Criação, Atualização, Exclusão) em qualquer uma de nossas coleções do banco de dados em uma nova coleção, AuditLog, e rotular cada registro com a ação que ocorreu. Bem simples.
No entanto, as ações de atualização representavam uma complicação: diferentes objetos no mecanismo de fluxo de trabalho podem ser atualizados de maneiras diferentes (por exemplo, usuários podem ser ativados, desativados ou conectados; plugins podem ser atualizados), e eu não queria sobrecarregar as próprias funções de atualização com lógica de registro. Eu precisava escrever uma função que recebesse como argumentos o estado anterior e posterior do documento que estava sendo modificado. Em seguida, com base nas alterações no documento, determinar a ação que as modificou. Mas qual deveria ser o tipo dos argumentos anterior e posterior?
class AuditLogsService { async log(before: ???, after: ???): Promise { //... } }
Uma solução teria sido usar uma interface. Nossos documentos ainda não implementam uma classe base ou uma interface, então isso pareceu simples. Criar uma interface `BaseDocument` e fazer com que todas as oito classes de documento a implementassem. Assim, os parâmetros `before` e `after` poderiam ser tipados para a interface.
interface AuditLogable {}
O problema dessa solução é que a interface seria sem sentido, pois nenhuma das classes de documento compartilha propriedades ou possui métodos. Bem, isso não é totalmente verdade – elas compartilham um campo "id". Então eu estaria modificando oito classes diferentes apenas para implementar uma interface com uma única propriedade e nenhum método.
interface AuditLogable { id: string; }
Que tal adicionar o método de registro (log) à interface e permitir que cada tipo de documento defina seu próprio comportamento de registro? Isso é orientação a objetos adequada, não é? Só que agora eu teria que implementar oito métodos de registro, cada um com comportamento praticamente idêntico. Por que não usar uma classe base em vez de uma interface para evitar repetições? Porque o comportamento de cada método de registro é apenas... majoritariamente Semelhante. Os plugins, por exemplo, são o único tipo de documento que não possui uma ação "criar". Em vez disso, chamamos de "instalar".
Percebi que estava abordando o problema de forma equivocada. Os documentos não deveriam realizar o registro de auditoria, mas sim ter o registro de auditoria realizado neles. Dessa forma, eu poderia escrever tudo em uma única função, sem implementar interfaces desnecessárias ou abstrações confusas.
Felizmente, o Typescript oferece uma maneira simples de construir esse padrão: Tipos de União. Com um tipo de união, não preciso abstrair nenhuma propriedade ou comportamento. Posso dizer efetivamente:,
“O argumento 'before' é de qualquer um dos tipos que podem ser registrados em auditoria.‘ type AuditLogable = User | Plugin | Playbook; // … class AuditLogsService { async log(before: AuditLogable, after: AuditLogable): Promise { //... } }
Como as informações de tipo são perdidas em tempo de execução, também preciso passar um parâmetro adicional indicando qual tipo da união estou passando como argumentos "antes" e "depois".
enum SwimType = { user, plugin, playbook, // ... } class AuditLogsService { async log(before: AuditLogable, after: AuditLogable, type: SwimType): Promise { //... } }
Agora implementei o AuditLogging com uma única função e um novo tipo de união. Nenhum dos documentos registrados precisa ser modificado, então adicionar uma nova classe ao logger é tão simples quanto adicioná-la ao AuditLogable e adicionar qualquer comportamento adicional necessário à função de registro. Tudo é fácil de testar e fácil de entender.

