Generics: funções e tipos parametrizados
1. Introdução aos Generics
Imagine que você precisa criar uma função que retorna o próprio elemento recebido. Sem generics, a tentativa mais comum é usar any:
function identity(value: any): any {
return value;
}
const result = identity("Hello");
// result é 'any' — perdemos todo o tipo!
O problema é claro: ao usar any, sacrificamos a segurança de tipos. O compilador não sabe se o retorno é string, number ou objeto. É aqui que os generics entram em cena.
Generics permitem criar funções, classes e tipos que funcionam com múltiplos tipos, mantendo a segurança e a integridade da tipagem. A sintaxe básica utiliza <T> (convenção para "Type"):
function identity<T>(value: T): T {
return value;
}
const result = identity("Hello"); // tipo inferido: string
const number = identity(42); // tipo inferido: number
O compilador TypeScript infere automaticamente o tipo T a partir do argumento passado, eliminando a necessidade de anotações manuais.
2. Funções Genéricas
A declaração de funções genéricas segue o padrão de adicionar <T> antes dos parâmetros:
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const first = firstElement([1, 2, 3]); // tipo: number | undefined
const firstStr = firstElement(["a", "b"]); // tipo: string | undefined
Podemos também especificar o tipo explicitamente, embora a inferência automática seja preferível na maioria dos casos:
const explicit = firstElement<string>(["x", "y"]); // explícito, mas redundante
3. Múltiplos Parâmetros de Tipo
Funções podem receber múltiplos parâmetros de tipo para lidar com diferentes tipos de entrada e saída:
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const merged = merge({ name: "Alice" }, { age: 30 });
// merged: { name: string } & { age: number }
// Acessos: merged.name (string), merged.age (number)
Outro exemplo prático é a criação de pares tipados:
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const p = pair("id", 123); // tipo: [string, number]
4. Constraints em Parâmetros de Tipo
Nem todo tipo é adequado para todas as operações. Constraints com extends limitam quais tipos são aceitos:
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength("Hello"); // 5 — string tem length
getLength([1, 2, 3]); // 3 — array tem length
// getLength(123); // Erro! number não tem length
Constraints também permitem acessar propriedades com segurança:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Bob", age: 25 };
getProperty(user, "name"); // string
getProperty(user, "age"); // number
// getProperty(user, "email"); // Erro! 'email' não é chave de user
5. Tipos Genéricos com Interfaces e Type Aliases
Interfaces genéricas permitem criar estruturas de dados reutilizáveis:
interface Container<T> {
value: T;
getValue(): T;
}
const stringContainer: Container<string> = {
value: "Hello",
getValue() { return this.value; }
};
const numberContainer: Container<number> = {
value: 42,
getValue() { return this.value; }
};
Type aliases genéricos são igualmente poderosos:
type Result<T> = {
data: T;
error?: string;
success: boolean;
};
type ApiResponse = Result<{ id: number; name: string }>;
// ApiResponse: { data: { id: number; name: string }; error?: string; success: boolean }
6. Generics com Classes
Classes genéricas combinam a flexibilidade de tipos com a orientação a objetos. Vamos implementar uma pilha genérica:
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
size(): number {
return this.items.length;
}
}
// Uso prático
const numberStack = new Stack<number>();
numberStack.push(10);
numberStack.push(20);
console.log(numberStack.pop()); // 20
const stringStack = new Stack<string>();
stringStack.push("TypeScript");
stringStack.push("Generics");
console.log(stringStack.peek()); // "Generics"
7. Padrões Avançados com Generics
Tipos condicionais permitem criar lógica de tipos baseada em condições:
type IsString<T> = T extends string ? "sim" : "não";
type Test1 = IsString<string>; // "sim"
type Test2 = IsString<number>; // "não"
Funções de alta ordem com generics possibilitam composição segura:
function pipe<T, U>(fn1: (arg: T) => U) {
return {
then<V>(fn2: (arg: U) => V) {
return pipe((arg: T) => fn2(fn1(arg)));
},
execute(arg: T): U {
return fn1(arg);
}
};
}
const doubleToString = pipe((n: number) => n * 2)
.then((n) => n.toString());
const result = doubleToString.execute(5); // "10" (string)
keyof com generics para acesso seguro a propriedades:
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map(item => item[key]);
}
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
];
const names = pluck(users, "name"); // string[]
const ids = pluck(users, "id"); // number[]
8. Boas Práticas e Armadilhas Comuns
Nomeação: Use T, U, V para casos simples. Para contextos mais específicos, use nomes descritivos:
// Ruim
function process<T, U>(a: T, b: U) {}
// Bom
function process<TInput, TOutput>(input: TInput): TOutput {}
Quando evitar generics: Se a função sempre opera com um tipo específico, não force generics. Simplicidade é prioridade:
// Desnecessário
function add<T extends number>(a: T, b: T): T { return (a + b) as T; }
// Melhor
function add(a: number, b: number): number { return a + b; }
Erros comuns:
- Esquecer constraints ao acessar propriedades de tipos genéricos
- Usar tipos muito amplos que permitem operações inseguras
- Confiar em inferência quando o contexto não é claro (especifique explicitamente nesses casos)
// Erro: T pode não ter .length
function logLength<T>(item: T) {
console.log(item.length); // Erro!
}
// Correto
function logLength<T extends { length: number }>(item: T) {
console.log(item.length); // OK
}
Generics são uma ferramenta essencial no TypeScript para escrever código reutilizável, flexível e seguro. Dominá-los permite criar abstrações poderosas sem sacrificar a tipagem estática que torna o TypeScript tão valioso.
Referências
- TypeScript Handbook: Generics — Documentação oficial completa sobre generics, com exemplos básicos e avançados
- TypeScript Generics Explained — Tutorial prático com exemplos passo a passo sobre funções e tipos genéricos
- Using Generics in TypeScript — Artigo técnico abordando padrões avançados e boas práticas com generics
- TypeScript: Conditional Types — Documentação oficial sobre tipos condicionais, extensão natural dos generics
- Generics with Classes in TypeScript — Tutorial focado em classes genéricas com exemplos de implementação de estruturas de dados