Restrições: extends, keyof e valores-padrão
O básico deixou um incômodo no ar. Dentro de uma função com um T totalmente livre, você quase nada pode fazer com o valor, porque o compilador não sabe nada sobre ele. Tente medir o tamanho de algo genérico:
function tamanho<T>(x: T): number { return x.length; // erro: a propriedade 'length' não existe no tipo 'T'}O erro é justo. T pode ser number, e number não tem length. Como o compilador não sabe qual tipo virá, ele precisa recusar tudo que não vale para todos os tipos possíveis, e quase nada vale para todos. Um T sem restrição é ótimo para carregar um valor de um lado a outro sem tocá-lo, como fazia a identidade, mas inútil quando você precisa olhar dentro dele.
A restrição resolve isso. Escrever T extends Algo diz ao compilador “aceito qualquer T, contanto que ele seja pelo menos um Algo”, e a partir daí você pode usar tudo que um Algo oferece, porque agora há uma garantia. A palavra extends aqui não é herança de classe; leia como “é atribuível a”, ou “se encaixa em”.
function tamanho<T extends { length: number }>(x: T): number { return x.length; // agora ok: todo T tem, no mínimo, um length numérico}
tamanho([1, 2, 3]); // ok, arrays têm lengthtamanho("olá"); // ok, strings têm lengthtamanho(42); // erro: number não satisfaz { length: number }Vale entender por que não basta escrever x: { length: number } direto, sem generic. A diferença aparece no retorno. Se o objetivo fosse só ler o length, o tipo fixo bastaria. Mas quando a função precisa devolver o que recebeu, o generic preserva o tipo exato, enquanto o tipo fixo o achataria:
// Com generic restrito: devolve o tipo específico que entrou.function maisComprido<T extends { length: number }>(a: T, b: T): T { return a.length >= b.length ? a : b;}
const r = maisComprido([1, 2], [1, 2, 3]);// ^? number[] (o tipo específico sobreviveu)
// Sem generic: o retorno esquece que era um array.function maisCompridoFrouxo(a: { length: number }, b: { length: number }) { return a.length >= b.length ? a : b;}
const r2 = maisCompridoFrouxo([1, 2], [1, 2, 3]);// ^? { length: number } (virou só "algo com length")Essa é a lição central da restrição: ela é o meio-termo entre o T livre, que preserva o tipo mas não deixa você tocá-lo, e o tipo fixo, que deixa tocar mas apaga a especificidade. Com extends, você toca e preserva.
O aquecimento: keyof e o acesso indexado
Section titled “O aquecimento: keyof e o acesso indexado”A restrição mais útil de todas envolve as chaves de um objeto, e para chegar nela é preciso primeiro dois operadores de tipo que não têm nada de genérico. Vale conhecê-los sobre um tipo concreto antes de misturá-los com parâmetros de tipo, senão a combinação parece mágica.
O operador keyof pega um tipo objeto e devolve a união das suas chaves, como um tipo:
type Pessoa = { nome: string; idade: number; ativo: boolean;};
type ChaveDePessoa = keyof Pessoa;// ^? "nome" | "idade" | "ativo"O acesso indexado faz o caminho complementar: dado um tipo e uma chave, devolve o tipo daquele campo. A sintaxe imita o acesso a propriedade em JavaScript, mas opera sobre tipos:
type TipoDaIdade = Pessoa["idade"];// ^? number
// A chave também pode ser uma união, e aí o resultado é a união dos tipos.type Valores = Pessoa[keyof Pessoa];// ^? string | number | booleanEsses dois operadores existem por si sós, sem generics. O que os torna poderosos é combiná-los com um parâmetro de tipo, para escrever funções que falam sobre “uma chave qualquer de um objeto qualquer” sem perder a precisão.
O clássico: getProperty
Section titled “O clássico: getProperty”Junte extends, keyof e o acesso indexado e você monta a função que lê uma propriedade de um objeto de forma totalmente tipada. Ela precisa de dois parâmetros de tipo amarrados um ao outro:
function pega<T, K extends keyof T>(obj: T, chave: K): T[K] { return obj[chave];}
const p = { nome: "Ana", idade: 30 };
const nome = pega(p, "nome");// ^? stringconst idade = pega(p, "idade");// ^? number
pega(p, "email"); // erro: "email" não é chave de pVale ler a assinatura em português, porque ela condensa quase tudo deste capítulo. T é o tipo do objeto. K extends keyof T diz que K é uma das chaves de T, o que garante, antes de rodar, que você só pode pedir chaves que existem: "email" é recusado no editor, não em produção. E o retorno T[K] diz que o tipo devolvido é exatamente o do campo pedido, então pega(p, "idade") sai como number e não como um string | number frouxo. A amarração entre T e K é o que faz a mágica: K não é qualquer string, é qualquer chave daquele T.
Valores-padrão de tipo
Section titled “Valores-padrão de tipo”Assim como um parâmetro de função pode ter um valor default, um parâmetro de tipo pode ter um tipo default, com a mesma sintaxe do =. Isso deixa o parâmetro opcional no ponto de uso: quem não passa nada recebe o default. O Result do capítulo anterior é um bom lugar para aplicar, já que o erro costuma ser um Error:
type Result<T, E = Error> = | { ok: true; valor: T } | { ok: false; erro: E };
type R1 = Result<number>;// erro é Error, porque E caiu no default
type R2 = Result<number, string>;// erro é string, porque E foi informadoO default pode inclusive depender de outro parâmetro declarado antes dele, o que serve para casos em que dois tipos coincidem na maioria das vezes:
// Por padrão, os dois lados do par têm o mesmo tipo;// quem quiser um par heterogêneo informa o segundo.type Par<A, B = A> = [A, B];
type Coordenada = Par<number>;// ^? [number, number]type Entrada = Par<string, number>;// ^? [string, number]Parâmetros const: preservar o literal
Section titled “Parâmetros const: preservar o literal”Por padrão, a inferência “alarga” (widening) os valores para o tipo mais geral: um array literal vira string[], um "pendente" vira string. Na maior parte do tempo isso é o que se quer, mas às vezes você precisa preservar o literal exato, por exemplo para tratar um array como uma tupla de valores fixos. Antes do TypeScript 5.0, o jeito era pedir as const em cada chamada. Desde o 5.0, um parâmetro de tipo const embute esse comportamento na assinatura:
function semConst<T>(x: T): T { return x;}function comConst<const T>(x: T): T { return x;}
const a = semConst(["norte", "sul"]);// ^? string[]
const b = comConst(["norte", "sul"]);// ^? readonly ["norte", "sul"]Com const T, quem chama não precisa lembrar de escrever as const: a inferência já preserva os literais e produz uma tupla readonly. É especialmente útil em bibliotecas, onde você quer que o usuário obtenha tipos precisos sem exigir cerimônia dele.
Restrições e type guards genéricos
Section titled “Restrições e type guards genéricos”Uma restrição também pode entrar num type guard, aquela função cujo retorno é um predicado x is T e que estreita o tipo de uma variável quando devolve true. Combinar predicado com generic permite escrever guardas reutilizáveis, parametrizados pelo tipo que verificam. Um guard que confirma “isto é um array cujos itens passam por este outro guard”:
function ehArrayDe<T>( valor: unknown, guardaDoItem: (item: unknown) => item is T,): valor is T[] { return Array.isArray(valor) && valor.every(guardaDoItem);}
const ehString = (x: unknown): x is string => typeof x === "string";
const dados: unknown = JSON.parse('["a", "b"]');
if (ehArrayDe(dados, ehString)) { // dentro deste bloco, 'dados' foi estreitado para string[] dados.map((s) => s.toUpperCase());}O T é inferido a partir do guard passado: como ehString prova x is string, o ehArrayDe conclui valor is string[]. Type guards genéricos são a ponte entre a fronteira insegura do programa, onde tudo é unknown (dados de rede, JSON.parse, entrada do usuário), e o interior tipado, e reaparecem quando o assunto for validação.
Com restrições, você deixou de apenas carregar tipos e passou a inspecioná-los e amarrá-los uns aos outros. O próximo capítulo dá o maior salto do curso: tipos que decidem, ramificando conforme a forma do que recebem, e que extraem tipos de dentro de outros. É onde extends reaparece, agora não como restrição, mas como uma pergunta.
Palavras-chave
Section titled “Palavras-chave”Operadores: extends, keyof, acesso indexado T[K], as const
Conceitos: restrição, parâmetro de tipo amarrado, valor-padrão de tipo, parâmetro const, widening, type guard genérico, predicado x is T
Exemplos: tamanho, maisComprido, pega/getProperty, Result com default, Par, ehArrayDe