Variance em generics
1. Introdução à Variance em Sistemas de Tipos
Variance é um conceito fundamental em sistemas de tipos com generics que define como a relação de subtipagem entre tipos concretos se propaga para tipos genéricos. Em outras palavras, dado que Gato é subtipo de Animal, podemos perguntar: Box<Gato> é subtipo de Box<Animal>? A resposta depende da variance do parâmetro T em Box<T>.
Existem quatro tipos de variance:
- Covariância: a relação de subtipagem é preservada na mesma direção
- Contravariância: a relação de subtipagem é invertida
- Invariância: não há relação de subtipagem entre os tipos genéricos
- Bivariância: ambos os sentidos são válidos (caso especial do TypeScript)
Compreender variance é essencial para projetar APIs seguras e prever o comportamento do sistema de tipos.
2. Covariância em Generics
Um parâmetro de tipo é covariante quando aparece apenas em posições de saída — ou seja, como tipo de retorno de métodos ou propriedades somente leitura. Nesse caso, se A é subtipo de B, então Producer<A> é subtipo de Producer<B>.
// Exemplo: Promise é covariante em T
type Animal = { nome: string };
type Gato = Animal & { mia(): void };
const promessaGato: Promise<Gato> = Promise.resolve({ nome: 'Mimi', mia: () => {} });
const promessaAnimal: Promise<Animal> = promessaGato; // OK: covariância
// Exemplo: Iterator (apenas leitura)
function processarAnimais(iter: Iterator<Animal>) {
// ...
}
const gatos: Gato[] = [{ nome: 'Mimi', mia: () => {} }];
const iteradorGatos = gatos[Symbol.iterator]();
processarAnimais(iteradorGatos); // OK: Iterator<Gato> é subtipo de Iterator<Animal>
Cuidado com mutabilidade: Arrays mutáveis no TypeScript são tratados como bivariantes por razões práticas, mas conceitualmente deveriam ser invariantes. Se Array<T> fosse covariante, poderíamos inserir um Cachorro em um Array<Gato> via referência de Array<Animal>:
const gatos: Gato[] = [gato1];
const animais: Animal[] = gatos; // TypeScript permite (bivariância)
animais.push(new Cachorro()); // Perigo! gatos agora contém um Cachorro
Por isso, prefira ReadonlyArray<T> ou readonly T[] para garantir covariância segura:
const gatosReadonly: readonly Gato[] = [gato1];
const animaisReadonly: readonly Animal[] = gatosReadonly; // OK: covariante e seguro
3. Contravariância em Generics
Um parâmetro é contravariante quando aparece apenas em posições de entrada — como parâmetros de funções ou métodos. Nesse caso, a relação de subtipagem é invertida: se A é subtipo de B, então Consumer<B> é subtipo de Consumer<A>.
// Exemplo: função comparadora
type Comparador<T> = (a: T, b: T) => number;
const compararAnimais: Comparador<Animal> = (a, b) => a.nome.localeCompare(b.nome);
const compararGatos: Comparador<Gato> = compararAnimais; // OK: contravariância
// Exemplo: EventHandler
type EventHandler<T> = (evento: T) => void;
const handlerAnimal: EventHandler<Animal> = (e) => console.log(e.nome);
const handlerGato: EventHandler<Gato> = handlerAnimal; // OK
// Demonstração com funções como valores
type FuncaoGato = (param: Gato) => void;
type FuncaoAnimal = (param: Animal) => void;
const fnAnimal: FuncaoAnimal = (animal) => console.log(animal.nome);
const fnGato: FuncaoGato = fnAnimal; // OK: FuncaoAnimal é subtipo de FuncaoGato
A intuição é: quem sabe processar qualquer Animal certamente sabe processar um Gato (que é um tipo mais específico). Por isso, (Animal) => void é subtipo de (Gato) => void.
4. Invariância e o Comportamento Padrão do TypeScript
Por padrão, parâmetros de tipo em TypeScript são invariantes. Isso significa que Box<Gato> não é subtipo nem supertipo de Box<Animal> — eles são tipos distintos.
class Caixa<T> {
constructor(private conteudo: T) {}
getConteudo(): T {
return this.conteudo;
}
setConteudo(valor: T): void {
this.conteudo = valor;
}
}
const caixaGato: Caixa<Gato> = new Caixa({ nome: 'Mimi', mia: () => {} });
// const caixaAnimal: Caixa<Animal> = caixaGato; // Erro: invariância
// caixaAnimal.setConteudo(new Cachorro()); // Isso seria catastrófico
A invariância é o comportamento mais seguro, pois evita que operações de escrita corrompam a integridade do tipo. Compare com Java, onde arrays são covariantes (e causam ArrayStoreException em runtime), ou C#, onde generics são invariantes por padrão mas permitem declaração explícita de variance.
5. Marcadores de Variance: in e out
TypeScript 4.7+ introduziu marcadores explícitos de variance para type aliases e interfaces, permitindo que o compilador verifique e documente a variance pretendida.
// out: parâmetro covariante (apenas saída)
type Produtor<out T> = {
produzir(): T;
readonly valor: T;
};
// in: parâmetro contravariante (apenas entrada)
type Consumidor<in T> = {
consumir(item: T): void;
};
// Exemplo completo: processador com entrada contravariante e saída covariante
type Processador<in T, out R> = (input: T) => R;
const processar: Processador<Animal, string> = (a) => a.nome;
const processarGato: Processador<Gato, string> = processar; // OK: contravariância em T
const processarString: Processador<Animal, string | number> = processar; // OK: covariância em R
// Interface com marcadores
interface Leitor<out T> {
ler(): T;
}
interface Escritor<in T> {
escrever(valor: T): void;
}
// Erro de compilação se violar a variance declarada
interface Invalido<out T> {
// setar(valor: T): void; // Erro: T em posição de entrada, mas declarado como out
}
6. Variance em Funções e Callbacks
Em tipos de função, a variance segue uma regra clara: parâmetros são contravariantes, retorno é covariante.
// Função de alta ordem
type Mapeador<T, R> = (item: T) => R;
function mapearArray<T, R>(arr: T[], fn: Mapeador<T, R>): R[] {
return arr.map(fn);
}
// Composição de funções
function compor<A, B, C>(
f: (a: A) => B,
g: (b: B) => C
): (a: A) => C {
return (x) => g(f(x));
}
// Exemplo com callbacks
function agendar<T>(
callback: (dados: T) => void,
timeout: number
): void {
setTimeout(() => callback({} as T), timeout);
}
// Armadilha comum: callback que espera tipo mais específico
function processarNumeros(callback: (n: number) => void): void {
callback(42);
}
const callbackGenerico: (valor: unknown) => void = (v) => console.log(v);
processarNumeros(callbackGenerico); // Erro: (unknown) => void não é subtipo de (number) => void
7. Casos Avançados e Limitações
Tipos recursivos:
interface TreeNode<out T> {
valor: T;
filhos: readonly TreeNode<T>[]; // Covariante por ser readonly
}
// Com contravariância
interface Predicado<in T> {
testar(item: T): boolean;
and(outro: Predicado<T>): Predicado<T>; // T em posição de entrada novamente
}
Interação com união e interseção:
// Covariância distribui sobre união
type Covariante<T> = { valor: T };
// Covariante<A | B> = Covariante<A> | Covariante<B>
// Contravariância distribui sobre interseção
type Contravariante<T> = (param: T) => void;
// Contravariante<A & B> = Contravariante<A> & Contravariante<B>
Limitação conhecida: métodos em classes são tratados como bivariantes no TypeScript por compatibilidade. Isso pode levar a situações inseguras:
class Colecao {
itens: Animal[] = [];
adicionar(item: Animal): void { this.itens.push(item); }
}
class ColecaoGatos extends Colecao {
// TypeScript permite override com tipo mais específico
adicionar(item: Gato): void { super.adicionar(item); }
}
const colecao: Colecao = new ColecaoGatos();
colecao.adicionar(new Cachorro()); // Perigo! Runtime error potencial
Boas práticas:
- Use ReadonlyArray<T> para garantir covariância
- Prefira readonly em propriedades de tipos genéricos
- Utilize marcadores in/out para documentar intenção
- Evite mutabilidade em tipos genéricos sempre que possível
8. Conclusão e Aplicações Práticas
Variance é um conceito sutil mas crucial para escrever código TypeScript seguro e expressivo. Resumindo:
- Covariância (
out): para produtores de dados (retorno de funções, leitura) - Contravariância (
in): para consumidores de dados (parâmetros de funções, callbacks) - Invariância (padrão): para tipos que leem e escrevem o mesmo tipo
- Bivariância: apenas em métodos de classes (por compatibilidade)
Ao projetar APIs com generics, pergunte-se: seu tipo é um produtor, consumidor ou ambos? Isso determinará a variance correta e evitará surpresas desagradáveis em tempo de compilação.
Para debugging de erros relacionados a variance, verifique:
1. Se o tipo genérico aparece em posições de entrada e saída simultaneamente
2. Se há mutabilidade desnecessária que força invariância
3. Se marcadores in/out estão corretamente aplicados
Referências
- TypeScript Handbook: Generics — Documentação oficial sobre generics no TypeScript, incluindo exemplos básicos de tipos genéricos
- TypeScript 4.7 Release Notes: Variance Annotations — Anúncio oficial dos marcadores
ineoutpara variance explícita - TypeScript Wiki: Type Compatibility — Explicação detalhada sobre subtipagem e variance no sistema de tipos do TypeScript
- Understanding Variance in TypeScript — Artigo técnico no Dev.to com exemplos práticos de covariância, contravariância e invariância
- TypeScript Deep Dive: Type Compatibility and Variance — Seção do livro "TypeScript Deep Dive" abordando variance com exemplos comparativos
- Covariance and Contravariance in TypeScript — Playground oficial do TypeScript com exemplos interativos de anotações de variance
- Wikipedia: Covariance and Contravariance in Computer Science — Referência teórica sobre os conceitos de variance em sistemas de tipos