Generics em TypeScript
Quase toda função útil tem uma parte que não depende do tipo com que trabalha. Pegar o primeiro elemento de uma lista é o mesmo gesto quer a lista seja de números, de strings ou de usuários. Guardar um valor numa caixa e devolvê-lo depois é a mesma mecânica seja qual for o valor. E é aqui que uma linguagem tipada enfrenta uma tensão real: se você escreve primeiro(lista: number[]): number, a função só serve para números, e você acaba copiando o mesmo corpo para cada tipo; se você escreve primeiro(lista: any[]): any para servir a todos, o tipo do resultado vira any, e a segurança que a tipagem prometia evapora no exato ponto em que ela seria mais útil.
Um generic é a saída dessa tensão. Ele deixa você escrever a função uma vez e ainda assim preservar o tipo, porque em vez de fixar o tipo na definição, você deixa um espaço reservado que quem chama a função preenche. Esse é o modelo mental que sustenta tudo o que vem depois, e vale internalizá-lo antes de qualquer sintaxe: um parâmetro de tipo é para os tipos o que um parâmetro comum é para os valores. Uma função recebe valores que só se conhecem na hora da chamada; uma função genérica recebe, além deles, um ou mais tipos que só se conhecem na hora da chamada. O <T> é a declaração desse parâmetro, e T é o nome pelo qual você se refere, dentro da função, ao tipo que ainda não sabe qual é.
O problema, concreto
Section titled “O problema, concreto”Antes da solução, vale sentir o problema na pele. Suponha uma função que devolve o primeiro elemento de um array. A tentativa que joga a segurança fora:
// any apaga o tipo: entra um array de strings, sai "qualquer coisa".function primeiro(lista: any[]): any { return lista[0];}
const nome = primeiro(["ana", "bia"]);nome.toFixed(2); // compila, e explode em runtime: string não tem toFixedO any não é um tipo, é a ausência de tipo: ele desliga a checagem. O resultado é que nome, que deveria ser string, aceita .toFixed(2) sem reclamar, e o erro só aparece quando o código roda. Você perdeu o autocomplete, perdeu a checagem, perdeu tudo que a tipagem existia para dar.
A reação natural de quem conhece TypeScript moderno é trocar any por unknown, o primo seguro. Ajuda, mas não resolve o problema certo:
// unknown é seguro, mas cego: ele obriga a checar, mas não sabe que// o resultado tem o mesmo tipo do que entrou.function primeiro(lista: unknown[]): unknown { return lista[0];}
const nome = primeiro(["ana", "bia"]);nome.toUpperCase(); // erro: 'nome' é 'unknown', o compilador não deixa usarO unknown conserta o buraco de segurança do any, mas cobra caro: ele esqueceu que o primeiro elemento de um string[] é uma string. A informação existia na entrada e foi descartada na saída. O que falta não é mais segurança nem menos, é uma forma de dizer “o tipo que sai é o mesmo tipo que entrou”, uma relação que nenhum tipo fixo consegue expressar. Essa relação é exatamente o que um generic captura:
// T é preenchido por quem chama. Aqui vira string, e o vínculo// entre a entrada e a saída fica registrado no tipo.function primeiro<T>(lista: T[]): T | undefined { return lista[0];}
const nome = primeiro(["ana", "bia"]);// ^? stringnome?.toUpperCase(); // okRepare que ninguém escreveu primeiro<string>(...). O TypeScript olhou o argumento, viu um string[], e concluiu sozinho que T é string. Essa dedução, a inferência do argumento de tipo, é o que torna generics agradáveis de usar em vez de verbosos, e é o primeiro assunto do curso.
O mapa do curso
Section titled “O mapa do curso”O percurso é uma escada: cada degrau assume o anterior e é pré-requisito do próximo. A ordem foi escolhida para que nenhum conceito apareça antes das peças que ele precisa.
O básico: o parâmetro de tipo fixa a fundação: como declarar um parâmetro de tipo, como a inferência o preenche, quando vale passá-lo à mão, e como generics vivem não só em funções mas em interfaces, type aliases e classes. É onde nasce o Result<T, E>, um dos exemplos que reaparecem no curso inteiro.
Restrições: extends, keyof e valores-padrão resolve a limitação óbvia do básico: um T totalmente livre é um T sobre o qual você quase nada pode fazer. Restringir com extends devolve poder ao parâmetro, e é aí que entram keyof, o acesso indexado T[K], os valores-padrão de tipo e os parâmetros const.
Tipos condicionais e infer é o maior salto do curso: tipos que decidem, ramificando com T extends U ? X : Y, distribuindo automaticamente sobre uniões, e extraindo tipos de dentro de outros com infer. É a porta de entrada da programação no nível de tipos.
Tipos mapeados mostra como transformar um tipo objeto chave a chave, tornando tudo opcional, somente-leitura ou anulável, e como renomear chaves no caminho. É o mecanismo por trás de metade dos utility types da biblioteca padrão.
Tipos de template literal leva a manipulação para o mundo das strings no nível de tipo: montar e desmontar strings tipadas, tipar nomes de eventos e rotas, e extrair pedaços de uma string com infer.
O kit da biblioteca padrão junta tudo reconstruindo, do zero, os utility types que você usa todo dia: Partial, Pick, Omit, Record, ReturnType, Awaited e companhia. Cada um vira um exercício de reconhecer qual primitiva dos capítulos anteriores o implementa.
Inferência: como o TypeScript decide os tipos volta ao tema do primeiro capítulo, agora no nível difícil: como a inferência flui através de funções de ordem superior, por que às vezes ela falha em silêncio e cai em unknown, e as ferramentas para guiá-la, incluindo satisfies, o contraste com sobrecargas e o NoInfer.
Variância, limites e padrões do mundo real fecha no teto: co e contravariância e as anotações in/out, por que TypeScript não tem “tipos de ordem superior” e o que isso impede, os limites de recursão do sistema de tipos, e os padrões que costuram o curso inteiro, do builder tipado ao event emitter.
O objetivo, ao fim dos oito, é que uma assinatura como function pick<T, K extends keyof T>(obj: T, chaves: K[]): Pick<T, K> deixe de ser um amontoado de símbolos e passe a se ler quase como uma frase, e que você saiba não só o que ela faz, mas por que cada peça está ali.
Palavras-chave
Section titled “Palavras-chave”Conceitos: generic, parâmetro de tipo, argumento de tipo, inferência, espaço reservado, reúso com segurança de tipo Contraste: any, unknown, tipo fixo, duplicação de função Temas do curso: restrições, tipos condicionais, infer, tipos mapeados, template literals, utility types, variância Exemplos recorrentes: primeiro/firstElement, Result, store tipado, event emitter tipado