Generics são type parameters. Uma função genérica declara um tipo marcador, normalmente chamado T, e cada chamada o preenche, então os tipos que entram decidem os tipos que saem. Uma única função passa a funcionar com números, strings e seus próprios objetos, e toda chamada continua totalmente tipada.
<T> depois do nome da função declara o type parameter. items: T[] o usa na entrada e T | undefined na saída, o que liga as duas. Você nunca escreveu number nem string: o TypeScript inferiu T a partir do argumento.
Por que não any ou unknown?
Sem generics, você tiparia o parâmetro como any ou unknown. Os dois aceitam qualquer array, e os dois perdem a ligação entre entrada e saída:
| Tipo do parâmetro | Aceita qualquer entrada | Tipo do resultado | Verificado |
|---|---|---|---|
any | sim | any | não: erros compilam |
unknown | sim | unknown | sim, mas você precisa fazer o narrowing antes de usar |
T (generic) | sim | ligado à entrada | sim, com o tipo preciso |
Inferência de argumentos de tipo
Normalmente o TypeScript infere os argumentos de tipo a partir dos valores que você passa. Você também pode escrevê-los explicitamente entre sinais de menor e maior na chamada, o que é necessário quando nada nos argumentos menciona T, ou quando a inferência escolhe algo que você não queria.
Dois resultados de inferência merecem atenção. pair(1, "one") sem o argumento explícito é um erro: o TypeScript infere T = number a partir do primeiro argumento e depois mostra Argument of type 'string' is not assignable to parameter of type 'number'. (TS2345). E emptyList() sem argumento nenhum recebe T = unknown, dando unknown[], o que raramente é o que você quer.
Vários type parameters
Uma função pode declarar quantos type parameters precisar, separados por vírgulas. Cada um é inferido de forma independente.
mapValues infere três coisas de uma vez: K é "tea" | "cake", V é number e R é string, a partir do retorno do callback. K extends string é uma restrição, tratada na próxima página.
Interfaces e type aliases genéricos
Tipos também podem receber parâmetros. Você então escreve o argumento ao usar o tipo: Box<number>, ApiResponse<User>. A maioria dos tipos de coleção nativos funciona assim: Array<T>, Map<K, V>, Promise<T>, Record<K, V>.
Result<T, E = string> também mostra um type parameter com valor padrão: Result<number> significa Result<number, string>. Como nos parâmetros opcionais de função, os parâmetros com padrão precisam vir depois dos obrigatórios.
Classes genéricas
Uma classe recebe type parameters depois do nome, e cada instância os fixa. Campos, métodos e parâmetros do construtor podem usá-los.
Se o construtor recebe um T, você pode omitir o argumento explícito: new Box(5) infere Box<number>. Membros estáticos pertencem à própria classe, não a uma instância, então não podem usar o type parameter da classe (Static members cannot reference class type parameters., TS2302).
Arrow functions e tipos de função genéricos
A lista de type parameters vai antes da lista de parâmetros. A mesma sintaxe descreve um tipo de função genérico.
const last = <T>(items: T[]): T | undefined => items[items.length - 1];
type Mapper = <T, R>(items: T[], fn: (item: T) => R) => R[];
const mapAll: Mapper = (items, fn) => items.map(fn);
// In a .tsx file, <T> looks like a JSX tag. Add a trailing comma:
const lastTsx = <T,>(items: T[]) => items[items.length - 1];
Inferindo literal types com const type parameters
Por padrão, um generic infere tipos alargados: pair("a", "b") dá [string, string]. Quando os valores exatos importam (nomes de rotas, nomes de eventos, listas de colunas), marque o parâmetro como const (TypeScript 5.0 e posteriores) e ele infere como se o argumento tivesse sido escrito as const:
Quem chama escreve um array comum, e a função mantém os literal types.
Quando não usar generics
Um type parameter vale a pena quando liga duas coisas: um parâmetro e o tipo de retorno, dois parâmetros, ou um parâmetro e um callback. Se T aparece só uma vez, ele não acrescenta nada, e um tipo simples é mais claro.
// Pointless: T is used once, so it is just a longer way to write unknown
function logValue<T>(value: T): void {
console.log(value);
}
// Clearer
function logValueSimple(value: unknown): void {
console.log(value);
}
// Also pointless: returns T but nothing connects T to an argument,
// so the caller is really just asserting a type
function parseJson<T>(text: string): T {
return JSON.parse(text);
}
O último é comum e enganoso: parseJson<User>(text) parece seguro, mas não verifica nada, exatamente como JSON.parse(text) as User. Retorne unknown e valide. Outros sinais de exagero: um type parameter que sempre recebe o mesmo argumento, ou um que poderia ser trocado por uma union de dois tipos conhecidos.
Para generics que precisam saber algo sobre T (que ele tem um length, ou que K é uma chave de T), o próximo passo são as restrições com extends.
Perguntas frequentes
O que são generics no TypeScript?
Generics são type parameters: marcadores como T que são preenchidos com um tipo real cada vez que uma função, interface, type alias ou classe é usada. function first<T>(items: T[]): T | undefined funciona com qualquer array, e o resultado tem o tipo dos elementos do array que você passou, então first([1, 2]) é number | undefined e first(["a"]) é string | undefined.
Qual é a diferença entre generics e any?
O any desliga a verificação de tipos: o valor que entra e o valor que sai não têm relação, e o resultado também é any. Um generic mantém a relação: identity<T>(x: T): T retorna exatamente o tipo que você passou. Use unknown se você aceita qualquer coisa mas não precisa devolver o tipo, e um generic se precisa.
O que significa <T> no TypeScript?
Declara um type parameter chamado T. Em function wrap<T>(value: T), T é uma variável de tipo que o TypeScript preenche a partir do argumento em cada chamada, ou que você passa explicitamente como wrap<string>("a"). O nome T é só uma convenção; nomes mais longos como TItem ou Key funcionam do mesmo jeito.
Como escrever uma arrow function genérica no TypeScript?
Coloque a lista de type parameters antes dos parâmetros: const first = <T>(items: T[]): T | undefined => items[0];. Em um arquivo .tsx, <T> seria lido como uma tag JSX, então escreva <T,> com uma vírgula no final, ou <T extends unknown>.
Como definir um tipo padrão para um generic no TypeScript?
Adicione = Type depois do parâmetro: interface ApiResponse<T = unknown> { data: T }. Assim ApiResponse sem argumento significa ApiResponse<unknown>. Parâmetros com padrão precisam vir depois dos que não têm, como os parâmetros opcionais de função.