Se você tem escrito TypeScript há algum tempo, mas os genéricos ainda parecem algo que você copia e cola do Stack Overflow sem entender completamente, este é o artigo para corrigir isso.
Genéricos são o recurso que leva o TypeScript de "JavaScript com anotações de tipo" para um sistema de tipos genuinamente expressivo. Quando clicam, você os verá em todo lugar — e vai recorrer a eles naturalmente em vez de usar any quando as coisas ficam difíceis. O
capítulo de genéricos do TypeScript Handbook
cobre o spec completo; este artigo foca nos padrões que você usará de verdade em codebases reais.
O Problema com any
Genéricos existem para resolver um problema específico: você quer que uma função ou estrutura de dados funcione com múltiplos tipos, mas não quer jogar fora as informações de tipo no processo.
A solução ingênua é any — e parece boa até você perceber o que você abriu mão:
// Com any — TypeScript não sabe o que sai
function identity(arg: any): any {
return arg;
}
const result = identity('hello');
// result é tipado como 'any' — você perdeu o tipo string
// TypeScript não vai capturar isso:
result.toFixed(2); // sem erro em tempo de compilação, crasha em tempo de execuçãoA informação de tipo entra, mas não sai. O que quer que você faça com result é completamente sem checagem. Isso não é um codebase TypeScript — é JavaScript com passos extras. Genéricos resolvem isso permitindo que você diga: "Eu não sei o tipo exato ainda, mas o que entra deve sair com o mesmo tipo."
// Com um genérico — T flui pela função
function identity<T>(arg: T): T {
return arg;
}
const result = identity('hello');
// result é tipado como 'string' ✅
result.toFixed(2); // ❌ TypeScript captura isso: Property 'toFixed' does not exist on type 'string'
const count = identity(42);
// count é tipado como 'number' ✅O <T> declara um parâmetro de tipo — pense nele como uma variável para tipos. TypeScript infere o que T é a partir do argumento que você passa, então você quase nunca precisa escrevê-lo explicitamente. identity<string>('hello') funciona, mas identity('hello') também — TypeScript descobre.
Funções Genéricas na Prática
A função identity é o exemplo canônico de ensino mas não é algo que você escreveria em produção. Aqui estão os tipos de funções genéricas que realmente aparecem em codebases reais:
// Um helper de primeiro/último em array com type-safety
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
const firstUser = first(users); // UserProfile | undefined
const lastOrder = last(orders); // Order | undefined
const firstNum = first([1, 2, 3]); // number | undefined
// Agrupar itens de array por uma chave
function groupBy<T, K extends string | number>(
items: T[],
getKey: (item: T) => K
): Record<K, T[]> {
return items.reduce((acc, item) => {
const key = getKey(item);
if (!acc[key]) acc[key] = [];
acc[key].push(item);
return acc;
}, {} as Record<K, T[]>);
}
// TypeScript infere T como Order e K como string
const byStatus = groupBy(orders, order => order.status);
// byStatus: Record<string, Order[]>
// byStatus['pending'] é Order[] ✅Note como TypeScript infere T e K automaticamente da forma como você chama a função. Você obtém type safety completa no valor de retorno sem escrever uma única anotação de tipo explícita no local de chamada. Esse é o retorno.
Interfaces e Tipos Genéricos
É aqui que os genéricos se tornam indispensáveis para o trabalho diário. Todo codebase que fala com uma API acaba precisando de alguns tipos wrapper genéricos. Aqui estão os que você verá (e escreverá) constantemente:
// Wrapper de resposta de API — usado em todos os endpoints
interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: string;
}
// Resposta de lista paginada
interface PaginatedResult<T> {
items: T[];
page: number;
pageSize: number;
totalItems: number;
totalPages: number;
}
// Usando-os com tipos concretos
interface UserProfile {
id: number;
name: string;
email: string;
avatarUrl: string;
}
interface Order {
id: number;
userId: number;
total: number;
status: 'pending' | 'shipped' | 'delivered';
}
// Os tipos de retorno são totalmente tipados — sem casting necessário downstream
async function getUser(id: number): Promise<ApiResponse<UserProfile>> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
async function listOrders(page: number): Promise<PaginatedResult<Order>> {
const res = await fetch(`/api/orders?page=${page}`);
return res.json();
}
const response = await getUser(1);
response.data.email; // ✅ string
response.data.avatarUrl; // ✅ string
const result = await listOrders(1);
result.items[0].status; // ✅ 'pending' | 'shipped' | 'delivered'
result.totalPages; // ✅ numberUserProfile, Order) automaticamente a partir de uma resposta de exemplo. Você
então os conecta nos seus wrappers genéricos.Restrições Genéricas
Às vezes você quer aceitar múltiplos tipos, mas não absolutamente qualquer tipo. Restrições genéricas permitem especificar o que um parâmetro de tipo deve ter. A sintaxe é
T extends SomeType — que significa "T deve ser atribuível a SomeType":
// Sem restrição — TypeScript não sabe que T tem uma propriedade id
function findById<T>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id); // ❌ Property 'id' does not exist on type 'T'
}
// Com restrição — T deve ter pelo menos um campo id: number
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id); // ✅
}
// Funciona com qualquer tipo que tenha id: number
const user = findById(users, 42); // UserProfile | undefined
const order = findById(orders, 101); // Order | undefined
// Outra restrição comum — T deve ser um objeto (exclui primitivos)
function mergeDefaults<T extends object>(partial: Partial<T>, defaults: T): T {
return { ...defaults, ...partial };
}
// T deve ter um name: string e email: string
function formatContact<T extends { name: string; email: string }>(contact: T): string {
return `${contact.name} <${contact.email}>`;
}
// Funciona em qualquer objeto com esses dois campos — UserProfile, Employee, qualquer coisa
formatContact({ name: 'Alice', email: '[email protected]', role: 'admin' }); // ✅A restrição não trava T em exatamente { id: number } —
significa que T deve ter pelo menos essa forma. Então passar um
UserProfile completo com dez campos está bem. Isso é tipagem estrutural em ação, que é
uma das mais poderosas e
bem documentadas
características do TypeScript.
A Restrição keyof
Um dos padrões genéricos mais úteis na biblioteca padrão do TypeScript e em codebases reais é combinar genéricos com keyof. Permite escrever funções que aceitam um nome de propriedade de um tipo de objeto e garantem que o tipo de retorno corresponde àquela propriedade.
// keyof T é a união de todas as chaves de T como literais de string
// K extends keyof T significa que K deve ser uma dessas chaves
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: UserProfile = {
id: 1,
name: 'Alice',
email: '[email protected]',
avatarUrl: 'https://example.com/avatar.png'
};
const name = getProperty(user, 'name'); // string ✅
const id = getProperty(user, 'id'); // number ✅
const email = getProperty(user, 'email'); // string ✅
getProperty(user, 'password'); // ❌ Argument of type '"password"' is not assignable to
// parameter of type 'keyof UserProfile'
// Caso de uso real: uma função de ordenação genérica
function sortBy<T>(items: T[], key: keyof T): T[] {
return [...items].sort((a, b) => {
const av = a[key];
const bv = b[key];
return av < bv ? -1 : av > bv ? 1 : 0;
});
}
const byName = sortBy(users, 'name'); // ✅ ordenado por nome
const byId = sortBy(users, 'id'); // ✅ ordenado por id
sortBy(users, 'nonexistent'); // ❌ capturado em tempo de compilaçãoParâmetros de Tipo Padrão
Assim como parâmetros de função podem ter padrões, parâmetros de tipo também podem. Isso é útil quando você tem um tipo genérico que é quase sempre usado com um tipo específico, mas você quer manter a flexibilidade para os casos em que não é.
// Parâmetro de tipo padrão — T assume string por padrão se não especificado
interface Cache<T = string> {
get(key: string): T | undefined;
set(key: string, value: T, ttlMs?: number): void;
delete(key: string): void;
clear(): void;
}
// Sem especificar T — usa o padrão (string)
declare const stringCache: Cache;
const val = stringCache.get('theme'); // string | undefined
// Especificando um T diferente
declare const userCache: Cache<UserProfile>;
const user2 = userCache.get('user:42'); // UserProfile | undefined
// Outro exemplo útil: um emissor de eventos com payload tipado
interface TypedEvent<TPayload = void> {
subscribe(handler: (payload: TPayload) => void): () => void;
emit(payload: TPayload): void;
}
// Eventos sem payload — o padrão void mantém a API limpa
const appReady: TypedEvent = { /* ... */ };
appReady.emit(); // nenhum argumento necessário
// Eventos com payload
const userLoggedIn: TypedEvent<{ userId: number; timestamp: Date }> = { /* ... */ };
userLoggedIn.emit({ userId: 42, timestamp: new Date() });Tipos Utilitários Genéricos Embutidos
TypeScript vem com um conjunto de tipos utilitários embutidos que são todos implementados usando os genéricos que você aprendeu acima. Entender como usá-los é uma das habilidades TypeScript mais práticas que você pode ter. Aqui está o conjunto central com exemplos reais mostrando o tipo de saída:
interface UserProfile {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'viewer';
passwordHash: string;
createdAt: Date;
}
// Partial<T> — todos os campos se tornam opcionais (ótimo para payloads PATCH)
type UpdateUserPayload = Partial<UserProfile>;
// { id?: number; name?: string; email?: string; role?: ...; ... }
// Required<T> — todos os campos se tornam obrigatórios (reverso de Partial)
interface DraftConfig {
apiUrl?: string;
timeout?: number;
maxRetries?: number;
}
type ResolvedConfig = Required<DraftConfig>;
// { apiUrl: string; timeout: number; maxRetries: number }
// Pick<T, K> — manter apenas os campos nomeados
type UserSummary = Pick<UserProfile, 'id' | 'name' | 'email'>;
// { id: number; name: string; email: string }
// Usar em visualizações de lista — envie apenas o que a UI precisa
// Omit<T, K> — excluir os campos nomeados
type PublicUserProfile = Omit<UserProfile, 'passwordHash'>;
// { id: number; name: string; email: string; role: ...; createdAt: Date }
// Seguro para incluir em respostas de API
// Record<K, V> — dicionário / mapa tipado
type PermissionMap = Record<UserProfile['role'], string[]>;
// { admin: string[]; user: string[]; viewer: string[] }
const permissions: PermissionMap = {
admin: ['read', 'write', 'delete', 'admin'],
user: ['read', 'write'],
viewer: ['read']
};
// ReturnType<T> — inferir o que uma função retorna (mantém tipos sincronizados automaticamente)
function buildUserSession(user: UserProfile) {
return {
token: crypto.randomUUID(),
userId: user.id,
role: user.role,
expiresAt: new Date(Date.now() + 3_600_000)
};
}
type UserSession = ReturnType<typeof buildUserSession>;
// { token: string; userId: number; role: 'admin' | 'user' | 'viewer'; expiresAt: Date }
// Mude buildUserSession e UserSession atualiza automaticamente ✅Se você está convertendo JavaScript existente para TypeScript e construindo esses tipos do zero, a ferramenta JS to TypeScript pode ajudá-lo a colocar os tipos iniciais no lugar para que você possa começar a colocar tipos utilitários em camadas por cima.
Um Exemplo Realista Fim-a-Fim: Cliente de API Tipado
Aqui está o padrão que eu uso toda vez que estou construindo uma camada de API tipada. Ele combina funções genéricas, interfaces genéricas e tipos utilitários em algo que te dá type safety fim-a-fim desde a chamada fetch até o componente consumindo os dados:
// O envelope de resposta — envolve cada resposta de API
interface ApiResponse<T> {
data: T;
meta: {
requestId: string;
duration: number;
};
}
// Envelope de erro — o que a API envia em caso de falha
interface ApiError {
code: string;
message: string;
details?: Record<string, string[]>;
}
// O wrapper fetch genérico
async function fetchApi<T>(
url: string,
options?: RequestInit
): Promise<ApiResponse<T>> {
const res = await fetch(url, {
headers: { 'Content-Type': 'application/json', ...options?.headers },
...options
});
if (!res.ok) {
const err: ApiError = await res.json();
throw new Error(`[${err.code}] ${err.message}`);
}
return res.json();
}
// Funções de endpoint tipadas — T é definido em cada local de chamada
async function getUser(id: number): Promise<UserProfile> {
const { data } = await fetchApi<UserProfile>(`/api/users/${id}`);
return data;
}
async function listOrders(userId: number, page = 1): Promise<PaginatedResult<Order>> {
const { data } = await fetchApi<PaginatedResult<Order>>(
`/api/users/${userId}/orders?page=${page}`
);
return data;
}
async function createOrder(
payload: Pick<Order, 'userId' | 'total'>
): Promise<Order> {
const { data } = await fetchApi<Order>('/api/orders', {
method: 'POST',
body: JSON.stringify(payload)
});
return data;
}
// Uso — totalmente tipado, sem casting em lugar algum
const user = await getUser(42);
console.log(user.email); // string ✅
const orders = await listOrders(42);
orders.items[0].status; // 'pending' | 'shipped' | 'delivered' ✅
orders.totalPages; // number ✅
const newOrder = await createOrder({ userId: 42, total: 99.99 });
newOrder.id; // number ✅Erros Comuns
Alguns padrões para evitar uma vez que você está confortável com genéricos:
- Usar
anydentro de um genérico. Se você escrevefunction wrap<T>(val: T): any, você derrotou o propósito. O ponto inteiro é que o tipo flui — usaranycomo tipo de retorno ou dentro da implementação significa que o TypeScript não pode mais rastreá-lo. - Sobre-restringir com tipos específicos. Escrever
function process<T extends UserProfile>quando você só precisa deT extends { id: number }é muito restritivo. Use a restrição mínima que faz sua implementação funcionar — assim a função permanece reutilizável em tipos diferentes que por acaso têm a mesma forma. - Usar genéricos quando uma união resolve. Se uma função aceita
stringounumbere a lógica é diferente para cada,function f(arg: string | number)com um type guard interno é mais limpo que um genérico. Genéricos brilham quando a mesma lógica se aplica a todas as variantes de tipo. - Parâmetros de tipo demais. Se você se encontrar escrevendo
<T, U, V, W>, dê um passo atrás. Isso geralmente é um sinal de que a função está fazendo muito, ou os tipos poderiam ser expressos como uma única interface. O código fonte do compilador TypeScript é uma boa referência — mesmo utilitários complexos raramente precisam de mais de 2–3 parâmetros de tipo.
Conclusão
Genéricos são o ponto onde o TypeScript para de ser "JavaScript anotado" e começa a ser um sistema de tipos genuinamente poderoso. A ideia central é simples: capture o tipo como um parâmetro para que ele flua pela sua função ou interface sem perder informações. A partir daí, restrições permitem que você reduza o que é aceitável, keyof te dá acesso type-safe a propriedades, e os tipos utilitários embutidos (Partial, Pick, Omit, Record) lidam com os padrões que você de outra forma digitaria manualmente. O melhor próximo passo é abrir um projeto real, encontrar uma função onde você usou any porque não sabia outra maneira, e substituí-la por um genérico. O
TypeScript Handbook sobre genéricos
e a
referência de tipos utilitários
são as duas páginas que valem um bookmark. E se você está construindo interfaces a partir de payloads reais de API,
JSON to TypeScript pode gerar os tipos concretos —
então é só uma questão de envolvê-los em seus tipos utilitários genéricos como
ApiResponse<T> e PaginatedResult<T> para obter type safety fim-a-fim completo.