Skip to content

O básico: o parâmetro de tipo

O ponto de partida é a função genérica, porque é onde o parâmetro de tipo aparece na sua forma mais limpa. O exemplo mínimo, tão pequeno que quase não faz nada, é a função identidade: recebe um valor e devolve o mesmo valor. O interesse dela não está no que faz, está em como preserva o tipo.

function identidade<T>(valor: T): T {
return valor;
}
const n = identidade(42);
// ^? number
const s = identidade("olá");
// ^? string

Vale dissecar essa assinatura devagar, porque cada peça reaparece em tudo que vem depois. O <T> logo após o nome é a declaração do parâmetro de tipo: é o momento em que você diz “esta função tem um tipo que só será conhecido na chamada, e vou chamá-lo de T”. Os dois T seguintes, em valor: T e no : T do retorno, são usos desse parâmetro: eles dizem “o argumento tem esse tipo, e o retorno tem esse mesmo tipo”. A diferença entre declarar e usar é a mesma de uma função comum, onde function f(x) declara o parâmetro x e o corpo o usa. Confundir os dois é a origem de metade da estranheza inicial com a sintaxe: o <T> na definição não recebe um tipo, ele cria um nome para o tipo que virá.

O que torna a função acima agradável é o que não foi preciso escrever. Ninguém digitou identidade<number>(42). O TypeScript olhou o argumento 42, viu um number, e concluiu que T é number naquela chamada. Essa dedução se chama inferência do argumento de tipo, e é a razão de generics serem, na prática, quase invisíveis no ponto de uso.

A inferência funciona olhando para os argumentos e resolvendo, de trás para frente, qual tipo faz a assinatura bater. Com o firstElement que apareceu no panorama, o mecanismo fica evidente:

function primeiro<T>(lista: T[]): T | undefined {
return lista[0];
}
const usuarios = [{ id: 1 }, { id: 2 }];
const u = primeiro(usuarios);
// ^? { id: number } | undefined

O argumento é um { id: number }[], o parâmetro é T[], então T só pode ser { id: number }. A partir daí o retorno T | undefined se resolve sozinho, e o autocomplete de u já conhece o campo id. Nada disso exigiu anotação: a informação estava no argumento, e a inferência a propagou até a saída.

Se a inferência quase sempre acerta, quando vale a pena escrever o argumento de tipo explicitamente, entre <>, na chamada? Em dois casos.

O primeiro é quando não há de onde inferir. Se o parâmetro de tipo não aparece em nenhum argumento, o TypeScript não tem pista alguma e cai no default unknown. Uma fábrica que só produz, sem consumir, é o caso clássico:

function criarLista<T>(): T[] {
return [];
}
const a = criarLista();
// ^? unknown[] (não havia argumento para inferir T)
const b = criarLista<number>();
// ^? number[] (aqui você disse qual era o T)

O segundo é quando a inferência acerta, mas acerta um tipo mais largo (ou mais estreito) do que você queria. Passar o tipo à mão é a forma de sobrepor a dedução automática:

// Sem anotação, T é inferido como o literal exato de cada argumento.
const x = identidade("pendente");
// ^? "pendente"
// Com anotação, você força o tipo mais largo que pretende usar depois.
const status = identidade<string>("pendente");
// ^? string

A regra prática que decorre disso: deixe a inferência trabalhar por padrão, e só alcance os <> quando ela não tiver de onde deduzir, ou quando deduzir algo diferente do que você precisa. Anotar por reflexo, em toda chamada, devolve a verbosidade que os generics existiam para eliminar.

Uma função pode declarar quantos parâmetros de tipo precisar, separados por vírgula, e cada um é inferido de forma independente. Uma função que emparelha dois valores num par precisa de dois:

function par<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
const p = par("idade", 30);
// ^? [string, number]

A foi inferido do primeiro argumento e B do segundo, sem que um interferisse no outro. Essa independência é o comportamento padrão, e há capítulos adiante que mostram como amarrar um parâmetro ao outro de propósito, com restrições. Por ora, o essencial é que múltiplos parâmetros de tipo são apenas múltiplos espaços reservados, preenchidos cada um pela sua posição.

Até aqui o parâmetro de tipo viveu em funções, mas o mesmo mecanismo se aplica a qualquer coisa que descreva uma estrutura. Uma interface genérica descreve a forma de um objeto que embrulha um valor de tipo variável:

interface Caixa<T> {
valor: T;
vazia: boolean;
}
const caixaDeTexto: Caixa<string> = { valor: "oi", vazia: false };

Repare que aqui, ao usar a interface, o <string> é obrigatório: Caixa sozinho não é um tipo completo, é um tipo que espera um argumento, do mesmo jeito que uma função espera um valor. Caixa<string> é o tipo completo. Essa é a diferença entre o lado da definição, onde <T> declara, e o lado do uso, onde <string> preenche.

Um type alias genérico faz o mesmo para qualquer tipo, não só para a forma de um objeto. É aqui que entra o primeiro dos exemplos que atravessam o curso: Result, uma forma de representar uma operação que ou deu certo com um valor, ou falhou com um erro, sem lançar exceção. Ele é uma união genérica de dois casos:

type Result<T, E> =
| { ok: true; valor: T }
| { ok: false; erro: E };
function dividir(a: number, b: number): Result<number, string> {
if (b === 0) return { ok: false, erro: "divisão por zero" };
return { ok: true, valor: a / b };
}
const r = dividir(10, 2);
if (r.ok) {
r.valor.toFixed(2); // aqui dentro o TS sabe que 'valor' existe e é number
} else {
r.erro.toUpperCase(); // e aqui sabe que 'erro' existe e é string
}

O Result<T, E> carrega dois parâmetros: T é o tipo do valor em caso de sucesso, E o tipo do erro em caso de falha. Quem usa preenche os dois conforme o caso, e o if (r.ok) faz o TypeScript estreitar a união para o ramo certo. Esse tipo reaparece nos capítulos de restrições, de condicionais e de padrões, ganhando helpers a cada passo; guarde-o.

Por fim, uma classe genérica parametriza os campos e métodos de uma classe pelo tipo que ela guarda. Uma pilha que serve para qualquer tipo de elemento:

class Pilha<T> {
private itens: T[] = [];
empilha(item: T): void {
this.itens.push(item);
}
desempilha(): T | undefined {
return this.itens.pop();
}
}
const pilha = new Pilha<number>();
pilha.empilha(1);
const topo = pilha.desempilha();
// ^? number | undefined

O <T> na classe se comporta como nas funções: declara um espaço reservado, e todo uso de T dentro da classe (o array itens, o argumento de empilha, o retorno de desempilha) se refere ao mesmo tipo, fixado quando você escreve new Pilha<number>(). A diferença é o alcance: numa função o parâmetro vive só durante a chamada; numa classe ele vive por toda a instância.

Com o parâmetro de tipo, a inferência e as quatro formas onde ele vive (função, interface, alias e classe), você tem a fundação. O próximo degrau ataca a limitação que já deve estar incomodando: 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. Restringir o T é o que devolve esse poder.

Conceitos: parâmetro de tipo, argumento de tipo, declaração vs uso, inferência, default unknown, apagamento em runtime Sintaxe: <T>, múltiplos parâmetros, argumento de tipo explícito Formas: função genérica, interface genérica, type alias genérico, classe genérica Exemplos: identidade, primeiro, par, Caixa, Result, Pilha