Tipos de Marca em TypeScript

Quando você modela entidades com TypeScript, é muito comum obter uma interface como esta:

TypeScript

 

O Problema

Os tipos das propriedades não têm significado semântico. Em termos de tipos, User.id, Order.id, Order.year, etc. são iguais: um número, e como número eles são intercambiáveis, mas semanticamente, não são.

Seguindo o exemplo anterior, podemos ter um conjunto de funções que executam ações sobre as entidades. Por exemplo:

TypeScript

 

Essas funções aceitarão qualquer número em qualquer argumento, independentemente do significado semântico do número. Por exemplo:

TypeScript

 

Obviamente, isso é um grande erro, e poderia parecer fácil evitar lendo o código, mas o código nem sempre é tão simples quanto o exemplo.

O mesmo acontece com getOrdersFiltered: podemos trocar os valores de dia e mês, e não receberemos nenhum aviso ou erro. Os erros acontecerão se o dia for maior que 12, mas é óbvio que o resultado não será o esperado.

A Solução

As regras de calistenia de objetos fornecem uma solução para isso: encapsular todos os tipos primitivos e Strings (Relacionado com o anti-padrão de obsessão primitiva). A regra é envolver os primitivos em um objeto que represente um significado semântico (DDD descreve isso como ValueObjects).

Mas com TypeScript, não precisamos usar classes ou objetos para isso: podemos usar o sistema de tipos para garantir que um número que represente algo diferente de um ano não possa ser usado no lugar de um ano.

Tipos Marcados

Este padrão utiliza a extensibilidade de tipos para adicionar uma propriedade que garante o significado semântico:

TypeScript

 

Esta simples linha cria um novo tipo que pode funcionar como um número — mas não é um número, é um ano.

TypeScript

 

Generalizando a Solução

Para evitar escrever um tipo por tipo marcado, podemos criar um tipo utilitário como:

TypeScript

 

Que utiliza um símbolo único como nome da propriedade de marca para evitar conflitos com suas propriedades e obtém o tipo original e a marca como parâmetros genéricos.

Com isso, podemos refatorar nossos modelos e funções da seguinte forma:

TypeScript

 

Agora, neste exemplo, a IDE mostrará um erro, pois id é um UserId e deleteOrder espera um OrderId.

TypeScript

 

Compromissos

Como um pequeno compromisso, você precisará usar X como Brand. Por exemplo, const year = 2012 as Year ao criar um novo valor a partir de um primitivo, mas isso é equivalente a um new Year(2012) se você usar objetos de valor. Você pode fornecer uma função que funcione como uma espécie de “construtor”:

TypeScript

 

Validação com Tipos Marcados

Tipos marcados também são úteis para garantir que os dados sejam válidos, pois você pode ter tipos específicos para dados validados, e pode confiar que o usuário foi validado apenas usando tipos:

TypeScript

 

Readonly não é obrigatório, mas para garantir que seu código não altere os dados após validá-los, é altamente recomendado.

Resumo

Tipos marcados são uma solução simples que inclui o seguinte:

  • Melhora a legibilidade do código: Torna mais claro qual valor deve ser usado em cada argumento
  • Confiabilidade: Ajuda a evitar erros no código que podem ser difíceis de detectar; agora o IDE (e a verificação de tipos) nos ajuda a detectar se o valor está no lugar correto
  • Validação de dados: Você pode usar tipos nomeados para garantir que os dados sejam válidos.

Você pode pensar em tipos nomeados como uma espécie de versão de ValueObjects, mas sem usar classes — apenas tipos e funções.

Desfrute do poder dos tipos!

Source:
https://dzone.com/articles/branded-types-in-typescript