Covariance e contravariance em TypeScript
1. Fundamentos da Variância em Sistemas de Tipos
Variância descreve como a relação de subtipos entre tipos complexos se comporta quando seus componentes variam. Em TypeScript, entender variância é crucial para escrever código type-safe.
Covariância preserva a direção da relação de subtipos: se A extends B, então X<A> extends X<B>. Por exemplo, se Gato extends Animal, então Array<Gato> extends Array<Animal>.
Contravariância inverte a direção: se A extends B, então X<B> extends X<A>. Isso ocorre principalmente em parâmetros de funções.
Invariância significa que não há relação de subtipos entre X<A> e X<B>, mesmo que A e B estejam relacionados.
2. Covariância no TypeScript: Posições de Saída
Tipos de retorno de funções são naturalmente covariantes:
class Animal { }
class Gato extends Animal { }
type Factory<T> = () => T;
const criarGato: Factory<Gato> = () => new Gato();
const criarAnimal: Factory<Animal> = criarGato; // ✅ Covariante: válido
Arrays em TypeScript são covariantes:
const gatos: Gato[] = [new Gato(), new Gato()];
const animais: Animal[] = gatos; // ✅ Covariante
Propriedades readonly também são seguramente covariantes:
type Resultado<T> = { readonly valor: T };
const resultadoGato: Resultado<Gato> = { valor: new Gato() };
const resultadoAnimal: Resultado<Animal> = resultadoGato; // ✅ Covariante
3. Contravariância no TypeScript: Posições de Entrada
Parâmetros de funções seguem contravariância para segurança de tipos:
type Manipulador<T> = (item: T) => void;
const manipularAnimal: Manipulador<Animal> = (animal: Animal) => {
animal.comer();
};
// ❌ Erro: Manipulador<Gato> não pode ser atribuído a Manipulador<Animal>
const manipularGato: Manipulador<Gato> = manipularAnimal;
// ✅ Correto: Manipulador<Animal> pode receber Manipulador<Gato>?
// Na verdade, é o contrário:
const manipuladorGenerico: Manipulador<Animal> = (gato: Gato) => {
gato.miar(); // Isso não seria seguro se recebesse um Cachorro
};
TypeScript 4.7+ permite declarar contravariância explicitamente com in:
type Callback<in T> = (arg: T) => void;
4. Invariância e o Dilema de Propriedades Mutáveis
Propriedades mutáveis criam invariância para evitar quebras em tempo de execução:
class Caixa<T> {
constructor(public item: T) {}
}
const caixaGato = new Caixa(new Gato());
const caixaAnimal: Caixa<Animal> = caixaGato; // ❌ Invariante: erro
// Por que isso é necessário?
// Se permitíssemos, poderíamos fazer:
caixaAnimal.item = new Cachorro(); // Quebra a caixa original de Gatos!
Comparação com readonly:
type CaixaLeitura<T> = { readonly item: T };
type CaixaEscrita<T> = { item: T };
const caixaLeitura: CaixaLeitura<Gato> = { item: new Gato() };
const caixaLeituraAnimal: CaixaLeitura<Animal> = caixaLeitura; // ✅ Covariante
const caixaEscrita: CaixaEscrita<Gato> = { item: new Gato() };
const caixaEscritaAnimal: CaixaEscrita<Animal> = caixaEscrita; // ❌ Invariante
5. Variância em Generics: Declaração Explícita com in e out
TypeScript infere automaticamente a variância, mas podemos declará-la explicitamente:
// Covariante: T aparece apenas em posição de saída
type Produtor<out T> = () => T;
// Contravariante: T aparece apenas em posição de entrada
type Consumidor<in T> = (arg: T) => void;
// Invariante: T aparece em ambas posições
type Transformador<T> = (arg: T) => T;
Exemplo prático com inferência automática:
type Callback<T> = (arg: T) => void; // TypeScript infere T como contravariante
type Retrieval<T> = () => T; // TypeScript infere T como covariante
// Verificação:
declare let cbGato: Callback<Gato>;
declare let cbAnimal: Callback<Animal>;
cbGato = cbAnimal; // ✅ Contravariante: Callback<Animal> é subtipo de Callback<Gato>
6. Implicações Práticas para APIs e Bibliotecas
ReadonlyArray vs Array:
function processarAnimais(animais: readonly Animal[]) {
// readonly permite covariância segura
const gatos: Gato[] = [new Gato()];
processarAnimais(gatos); // ✅ Covariante com readonly
}
function modificarAnimais(animais: Animal[]) {
const gatos: Gato[] = [new Gato()];
modificarAnimais(gatos); // ⚠️ Permitido, mas pode causar problemas
}
React Components:
interface ButtonProps {
label: string;
onClick: () => void;
}
// React.FC é covariante em Props
const Button: React.FC<ButtonProps> = ({ label, onClick }) => (
<button onClick={onClick}>{label}</button>
);
Refatoração para covariância:
// Invariante
interface EstadoMutavel<T> {
valor: T;
setValor: (novo: T) => void;
}
// Covariante
interface EstadoLeitura<out T> {
readonly valor: T;
}
7. Casos Complexos: Bivariância e Limitações do TypeScript
Em modo strictFunctionTypes: false, parâmetros de função são bivariantes (aceitam ambas direções):
// Com strictFunctionTypes: false
type Handler<T> = (arg: T) => void;
const handlerAnimal: Handler<Animal> = (a: Animal) => {};
const handlerGato: Handler<Gato> = handlerAnimal; // ✅ Permitido (bivariante)
Métodos em classes são bivariantes por padrão, mesmo com strictFunctionTypes: true:
class Colecao {
adicionar(item: Animal) {}
}
const colecaoGato: Colecao = new Colecao();
// Métodos são verificados bivariantemente
Para corrigir e tornar contravariante:
interface ColecaoSegura {
adicionar: (item: Animal) => void; // Função, não método
}
const colecaoSegura: ColecaoSegura = {
adicionar: (item: Animal) => {}
};
Limitações com tipos condicionais:
type Condicional<T> = T extends Animal ? Gato : never;
// Variância não é preservada em tipos condicionais complexos
Referências
- TypeScript Handbook: Functions - Variance — Documentação oficial sobre variância em funções e parâmetros
- TypeScript 4.7 Release Notes: Variance Annotations — Anotações de variância com
ineoutintroduzidas no TypeScript 4.7 - TypeScript Deep Dive: Covariance and Contravariance — Guia prático com exemplos detalhados sobre variância
- Understanding TypeScript's Type System: Variance — Artigo técnico explicando variância com exemplos do mundo real
- TypeScript: Variance and Assignability — Tutorial sobre como a variância afeta a atribuição de tipos em TypeScript
- Microsoft Docs: Covariance and Contravariance in Generics — Conceitos fundamentais de variância aplicados a sistemas de tipos genéricos