en
· 4 min de leitura

Tipos nas bordas: valide uma vez, confie em todo lugar

Tipos do TypeScript somem em runtime. Coloque uma checagem real em cada borda e deixe o compilador levar a prova para dentro.

fullstacktypescript

TypeScript tem um segredo sujo que todo engenheiro sênior conhece e todo júnior aprende do jeito difícil: os tipos desaparecem em tempo de execução. Uma função tipada para receber um User vai aceitar de bom grado o que quer que a rede mandou, e o compilador não estará lá para reclamar.

Então a pergunta não é se validar. É onde. Minha resposta é a mesma em todo sistema que desenho: valide em cada borda, uma vez, e nunca mais lá dentro.

O que conta como borda

Borda é qualquer ponto onde dados entram no seu programa vindos de algo que você não controla. As óbvias são corpos de requisição HTTP, query strings e parâmetros de rota. As menos óbvias são argumentos de Server Actions, que chegam do navegador e são exatamente tão confiáveis quanto um corpo de requisição. Webhooks de provedores de pagamento. Mensagens tiradas de uma fila. Linhas lidas de um banco cujo schema outra pessoa pode migrar. Variáveis de ambiente. Arquivos que um usuário enviou. Respostas de uma API de terceiro que mudou de formato sem mudar a versão.

Cada um desses é uma fronteira onde uma anotação de tipo é uma esperança, não uma garantia. A anotação diz o que você espera. Só uma checagem em runtime diz o que você recebeu.

O padrão

Em cada borda eu defino um schema com uma biblioteca de validação em runtime, e infiro o tipo TypeScript a partir desse schema. Esse é o movimento chave: uma definição, dois usos. O schema valida em runtime e o tipo flui pelo compilador. Eles não podem discordar porque são o mesmo objeto.

O valor parseado é a única coisa que cruza para o interior. A entrada crua nunca cruza. Se um handler recebe unknown, chama parse e passa o resultado adiante, toda função abaixo pode confiar completamente nos tipos dos parâmetros. Não há segunda camada de validação, não há checagens defensivas de null no meio da regra de negócio, não há coerção 'por via das dúvidas'.

Essa confiança interna é a recompensa. É o que torna o código legível. Uma função de precificação que recebe um Order validado pode gastar suas linhas com preço, não se perguntando se quantity é uma string.

Os detalhes que importam

Faça parse, não só validação. Um validador que retorna booleano te deixa com o valor original e uma promessa. Um parser retorna um valor novo do tipo estreitado, com defaults aplicados e campos desconhecidos removidos. Remover campos desconhecidos é uma propriedade de segurança: impede que um cliente contrabandeie uma flag isAdmin em um update.

Falhe alto na borda, baixo em lugar nenhum. Uma requisição malformada recebe 400 com um erro estruturado. Um webhook malformado é logado com o payload cru e rejeitado. Uma variável de ambiente malformada derruba o processo na inicialização, antes de servir uma única requisição, que é o momento mais barato possível para descobrir.

Valide o que você envia também. Quando seu servidor responde a um cliente ou publica um evento, essa saída é a borda de outra pessoa. Checar contra um schema antes de sair pega o bug em que um refactor mudou o nome de um campo e todos os consumidores quebraram de uma vez.

Branded types fecham o ciclo. Depois que uma string de email passou pelo schema de email, dê a ela um branded type para que uma função que exige email validado não possa ser chamada com uma string crua. O compilador agora rastreia quais valores foram checados, e a prova viaja com o valor.

O que me recuso a fazer

Não valido duas vezes. Validação dupla é sinal de que ninguém confia na primeira checagem, e significa que o interior está cheio de guardas redundantes escondendo a lógica real.

Não uso type assertions na borda. Escrever as User no resultado de um fetch é mentir para o compilador, e o compilador acredita em você. Em toda auditoria em que encontrei dados corrompidos no banco, encontrei uma assertion em algum lugar acima.

E não pulo a borda porque 'é a nossa própria API'. Sua própria API é construída por uma pessoa que vai mudá-la numa terça-feira. A checagem de schema é como você descobre na terça em vez de um mês depois, por um cliente.