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