Constraints em generics com extends
1. O que são Constraints em Generics?
Constraints em generics são mecanismos que permitem restringir quais tipos podem ser utilizados como argumento para um parâmetro genérico. A palavra-chave extends é usada para definir esses limites, garantindo que apenas tipos que satisfaçam determinada condição sejam aceitos.
Sem constraints, um parâmetro genérico aceita qualquer tipo:
function obterTamanho<T>(valor: T): number {
// Erro! TypeScript não sabe se 'valor' tem propriedade 'length'
return valor.length;
}
Com constraints, podemos garantir operações seguras:
function obterTamanho<T extends { length: number }>(valor: T): number {
// Agora TypeScript sabe que 'valor' tem 'length'
return valor.length;
}
obterTamanho("texto"); // 5
obterTamanho([1, 2, 3]); // 3
obterTamanho(123); // Erro! number não tem length
A principal diferença é que constraints transformam generics de "qualquer tipo" para "tipos que possuem características específicas", permitindo acesso seguro a propriedades e métodos.
2. Sintaxe Básica de Constraints
A sintaxe fundamental é T extends TipoBase, onde TipoBase define o limite do que é aceito:
function exibirValor<T extends string | number>(valor: T): string {
return `Valor: ${valor}`;
}
exibirValor("Olá"); // "Valor: Olá"
exibirValor(42); // "Valor: 42"
exibirValor(true); // Erro! boolean não é string | number
Constraints com tipos primitivos são úteis para operações matemáticas ou de string:
function somar<T extends number>(a: T, b: T): T {
return (a + b) as T;
}
console.log(somar(10, 20)); // 30
console.log(somar("10", "20")); // Erro! string não é number
3. Constraints com Interfaces e Tipos de Objetos
Quando trabalhamos com objetos, podemos usar interfaces para definir constraints mais específicas:
interface Identificavel {
id: number;
nome: string;
}
function exibirInfo<T extends Identificavel>(item: T): string {
return `${item.nome} (ID: ${item.id})`;
}
const usuario = { id: 1, nome: "Maria", email: "maria@email.com" };
const produto = { id: 100, nome: "Notebook", preco: 2500 };
console.log(exibirInfo(usuario)); // "Maria (ID: 1)"
console.log(exibirInfo(produto)); // "Notebook (ID: 100)"
console.log(exibirInfo({ id: "abc", nome: "Teste" })); // Erro! id precisa ser number
Constraints permitem acessar propriedades de forma segura dentro de funções genéricas:
interface ComTamanho {
tamanho: number;
}
function maiorQue<T extends ComTamanho>(a: T, b: T): T {
return a.tamanho > b.tamanho ? a : b;
}
const arr1 = [1, 2, 3];
const arr2 = [4, 5];
console.log(maiorQue(arr1, arr2)); // [1, 2, 3] (tamanho 3 > 2)
4. Constraints com Classes e Herança
Classes podem servir como constraints, garantindo que tipos tenham métodos e propriedades específicos:
class Animal {
constructor(public nome: string) {}
emitirSom(): string {
return "Som genérico";
}
}
class Cachorro extends Animal {
emitirSom(): string {
return "Au au!";
}
}
class Gato extends Animal {
emitirSom(): string {
return "Miau!";
}
}
function apresentar<T extends Animal>(animal: T): string {
return `${animal.nome} diz: ${animal.emitirSom()}`;
}
console.log(apresentar(new Cachorro("Rex"))); // "Rex diz: Au au!"
console.log(apresentar(new Gato("Mimi"))); // "Mimi diz: Miau!"
Constraints com classes permitem polimorfismo seguro:
class Repositorio<T extends { id: number }> {
private itens: T[] = [];
adicionar(item: T): void {
this.itens.push(item);
}
buscarPorId(id: number): T | undefined {
return this.itens.find(item => item.id === id);
}
}
interface Produto {
id: number;
nome: string;
preco: number;
}
const repo = new Repositorio<Produto>();
repo.adicionar({ id: 1, nome: "Mouse", preco: 50 });
console.log(repo.buscarPorId(1)); // { id: 1, nome: "Mouse", preco: 50 }
5. Constraints com Union Types e Type Guards
Union types combinados com type guards oferecem flexibilidade adicional:
function processar<T extends string | string[]>(dado: T): number {
if (Array.isArray(dado)) {
// Type guard: TypeScript sabe que é string[]
return dado.length;
}
// TypeScript sabe que é string
return dado.length;
}
console.log(processar("TypeScript")); // 10
console.log(processar(["a", "b", "c"])); // 3
Exemplo mais complexo com múltiplos tipos:
type Resposta = { sucesso: true; dados: unknown } | { sucesso: false; erro: string };
function tratarResposta<T extends Resposta>(resposta: T): string {
if (resposta.sucesso) {
return `Operação bem-sucedida: ${JSON.stringify(resposta.dados)}`;
}
return `Erro: ${resposta.erro}`;
}
console.log(tratarResposta({ sucesso: true, dados: { id: 1 } }));
console.log(tratarResposta({ sucesso: false, erro: "Não encontrado" }));
6. Constraints Múltiplas e Aninhadas
Constraints podem ser combinadas usando o operador & (intersection):
interface SerVivo {
respirar(): void;
}
interface Trabalhador {
trabalhar(): string;
}
function realizarAtividades<T extends SerVivo & Trabalhador>(entidade: T): string {
entidade.respirar();
return entidade.trabalhar();
}
class Humano implements SerVivo, Trabalhador {
respirar(): void {
console.log("Respirando...");
}
trabalhar(): string {
return "Trabalhando...";
}
}
const pessoa = new Humano();
console.log(realizarAtividades(pessoa)); // "Trabalhando..."
Constraints aninhadas permitem dependências entre parâmetros genéricos:
function parear<T extends U, U extends { id: number }>(item: T, base: U): T {
return { ...item, id: base.id };
}
interface Base {
id: number;
}
interface Detalhes extends Base {
nome: string;
descricao: string;
}
const detalhes: Detalhes = { id: 1, nome: "Produto", descricao: "Descrição" };
const resultado = parear(detalhes, { id: 100 });
console.log(resultado); // { id: 100, nome: "Produto", descricao: "Descrição" }
7. Erros Comuns e Boas Práticas
Erro comum: não respeitar a constraint
interface ComNome {
nome: string;
}
function saudacao<T extends ComNome>(obj: T): string {
return `Olá, ${obj.nome}!`;
}
saudacao({ nome: "João" }); // OK
saudacao({ idade: 30 }); // Erro! 'idade' não tem 'nome'
Evitar uso excessivo de any:
// Ruim: perde segurança de tipos
function processarRuim<T>(dado: T): any {
return (dado as any).nome;
}
// Bom: usa constraint adequada
function processarBom<T extends { nome: string }>(dado: T): string {
return dado.nome;
}
Boas práticas para constraints reutilizáveis:
// Definir interfaces base
interface Serializavel {
toJSON(): string;
}
interface Validador {
validar(): boolean;
}
// Constraints reutilizáveis
type Salvavel = Serializavel & Validador;
function salvar<T extends Salvavel>(item: T): boolean {
if (!item.validar()) {
return false;
}
localStorage.setItem("dados", item.toJSON());
return true;
}
class Usuario implements Salvavel {
constructor(private nome: string) {}
toJSON(): string {
return JSON.stringify({ nome: this.nome });
}
validar(): boolean {
return this.nome.length > 0;
}
}
const usuario = new Usuario("Ana");
console.log(salvar(usuario)); // true
Dicas finais:
- Prefira constraints específicas a any para manter a segurança de tipos
- Use interfaces para definir contratos claros
- Combine constraints com type guards para lógicas condicionais
- Mantenha constraints simples e focadas em comportamentos necessários
Constraints com extends são fundamentais para criar código TypeScript seguro, reutilizável e expressivo, permitindo que generics mantenham sua flexibilidade sem sacrificar a segurança de tipos.
Referências
- TypeScript: Generics - Constraints — Documentação oficial do TypeScript sobre constraints em generics com exemplos práticos
- TypeScript Deep Dive: Generics — Guia abrangente sobre generics em TypeScript, incluindo constraints avançadas
- TypeScript Generics: A Complete Guide — Tutorial completo da DigitalOcean sobre generics com exemplos de constraints
- Understanding TypeScript Generics — Explicação detalhada do freeCodeCamp sobre generics e constraints com extends
- TypeScript Handbook: Generics — Referência oficial completa sobre todos os aspectos de generics em TypeScript
- TypeScript Generics with Classes and Interfaces — Tutorial prático sobre uso de generics com classes, interfaces e constraints