en
· 4 min de leitura

Generics em TypeScript que ajudam, e os que atrapalham

Um generic que relaciona entrada e saída é um presente. Um que aparece uma vez na assinatura é uma mentira que o compilador não pega.

fullstacktypescript

Generics são onde o código TypeScript vai para ficar ou muito claro ou completamente ilegível, e já revisei os dois no mesmo arquivo. A diferença não é habilidade. É se o generic está fazendo um trabalho que só um generic consegue fazer.

Este é o teste que aplico, e leva cinco segundos: o parâmetro de tipo aparece pelo menos duas vezes na assinatura? Se ele relaciona uma entrada a uma saída, ou duas entradas entre si, está merecendo o lugar. Se aparece uma vez, é uma fantasia para unknown, e a função seria mais clara sem ele.

Os que ajudam

O melhor generic relaciona o que entra com o que sai. Uma função que recebe um array de T e retorna o primeiro T. Um fetcher que recebe um schema e retorna o tipo inferido do schema. Um método de repositório que recebe o nome de uma entidade e retorna o tipo de linha daquela entidade. Em cada caso, quem chama não escreve anotação e recebe um tipo preciso de volta, porque a inferência o carrega.

O segundo bom uso é restringir uma relação entre argumentos. Uma função que recebe um objeto e uma chave desse objeto, tipada como K extends keyof T, não pode ser chamada com uma chave que não existe. Isso é um generic fazendo o trabalho de uma checagem de runtime em tempo de compilação, e é o padrão por trás de toda biblioteca de formulários e todo query builder bem tipados.

O terceiro é um contêiner tipado. Um tipo Result com ramo de sucesso T e ramo de erro E. Uma resposta paginada de T. Esses são genéricos porque a forma é a mesma e o conteúdo varia, que é exatamente para o que generics foram inventados.

Nos três, o parâmetro de tipo é inferido no ponto de chamada. Se quem chama precisa escrever os colchetes angulares à mão toda vez, o generic provavelmente não está ajudando.

Os que atrapalham

O generic de uso único é o mais comum. Uma função declarada como recebendo T e retornando void, onde T nunca é restringido nem relacionado a nada. O autor queria dizer 'isto aceita qualquer coisa', e unknown diz isso com honestidade. O generic diz com cerimônia e sugere uma relação que não existe.

O generic que substitui uma união é o segundo. Um componente que recebe um parâmetro de tipo para a variante, quando a variante é uma de quatro strings conhecidas. Uma união de literais é mais curta, produz mensagens de erro melhores, e o compilador consegue checar exaustividade sobre ela. Generics não enumeram.

O generic condicional profundo é o terceiro e o pior. Tipos que recursam por formas de objeto, distribuem sobre uniões e inferem por template literals para computar um resultado. São impressionantes. Também são lentos de checar, impossíveis de depurar quando falham, e produzem mensagens de erro que enchem a tela. Já vi um time perder dois dias com um tipo que tentava derivar o formato de validação de um formulário a partir da árvore de componentes. Um tipo escrito à mão levaria dez minutos e seria legível para sempre.

O quarto é o generic que vaza. Uma função retorna um T que nunca foi estreitado, então quem chama recebe um tipo tecnicamente correto e praticamente inútil, e recorre a uma assertion para torná-lo usável. Essa assertion é onde o bug de runtime vai estar.

As regras que aplico em revisão

Um parâmetro de tipo precisa ser usado pelo menos duas vezes, ou vira unknown. Um parâmetro de tipo precisa ser inferível a partir dos argumentos, ou a função ganha um overload no lugar. Um conditional type com mais de dois níveis de profundidade ganha nome, um comentário com exemplo de entrada e saída, e um teste usando um helper de expect-type.

Prefira satisfies e const assertions a generics quando o objetivo é checar um objeto literal contra uma forma sem perder a precisão dele. Prefira uma discriminated union a um generic quando o conjunto de opções é conhecido. Use o utilitário NoInfer quando um parâmetro deve ser checado contra um tipo já inferido em vez de alargá-lo.

E na dúvida, escreva a versão concreta primeiro. Duas funções concretas que compartilham uma forma vão te dizer qual deveria ser o generic. Uma função abstrata escrita antes de os casos concretos existirem vai te dizer o que o autor esperava que a forma fosse, e esperança não é tipo.