La varianza describe cómo se relacionan los subtipos cuando se componen dentro de tipos genéricos. Entenderla es fundamental para escribir código TypeScript seguro porque determina si una función acepta o rechaza ciertos tipos en posiciones de entrada o de salida.
Covarianza: los subtipos son aceptables en salida
Un tipo es covariante cuando un subtipo puede usarse donde se espera el tipo base. En TypeScript, las posiciones de retorno son covariantes:
class Animal { nombre: string = ''; }
class Perro extends Animal { raza: string = ''; }
// Covarianza en la posición de retorno:
type ObtenerAnimal = () => Animal;
type ObtenerPerro = () => Perro;
// ObtenerPerro es asignable a ObtenerAnimal porque Perro extends Animal:
const f: ObtenerAnimal = (): Perro => new Perro(); // OK
Contravarianza: los supertipos son aceptables en entrada
Los parámetros de función son contravariantes: si una función acepta Animal, también debería aceptar Perro, pero no al revés:
type ProcesarAnimal = (animal: Animal) => void;
type ProcesarPerro = (perro: Perro) => void;
// ProcesarAnimal es asignable a ProcesarPerro (no al revés):
const procesarPerro: ProcesarPerro = (a: Animal) => {
console.log(a.nombre); // OK: accede solo a Animal
};
// Al revés NO es seguro:
// const procesarAnimal: ProcesarAnimal = (p: Perro) => {
// console.log(p.raza); // Error en runtime si se pasa un Animal
// };
strictFunctionTypes y la trampa de bivarianza
Sin strictFunctionTypes, TypeScript trataba los parámetros de función como bivariantes (aceptaban tanto subtipos como supertipos), lo que permitía asignaciones inseguras. Con la opción activada (incluida en strict), los métodos de interfaz siguen siendo bivariantes, pero las funciones con sintaxis de flecha son contravariantes en sus parámetros:
// Con strictFunctionTypes:
interface Repositorio {
// Sintaxis de método bivariante (permitido por compatibilidad):
guardar(entidad: Animal): void;
}
// Función con flecha contravariante (comprobación estricta):
type GuardarFn = (entidad: Animal) => void;
// Error con strictFunctionTypes:
// const fn: GuardarFn = (p: Perro) => {}; // Error: Perro no es contravariante
La trampa de los arrays mutables
Los arrays en TypeScript son covariantes aunque sean mutables, lo que puede romper la seguridad de tipos:
const perros: Perro[] = [new Perro()]; // Esto compila pero es inseguro: const animales: Animal[] = perros; // OK por covarianza de arrays animales.push(new Animal()); // Añade un Animal a lo que realmente es Perro[] const perro = perros[1]; // perro es Perro según TS pero en runtime es Animal console.log(perro.raza); // undefined en runtime // Solución: usar ReadonlyArray para indicar que no se va a mutar: const animalesRO: ReadonlyArray<Animal> = perros; // OK y seguro // animalesRO.push(new Animal()); // Error: push no existe en ReadonlyArray
Invarianza con genéricos en escritura y lectura
Un tipo genérico es invariante cuando se usa tanto en posición de entrada como de salida. Por ejemplo, una clase con getter y setter del mismo tipo:
class Caja<T> {
constructor(private valor: T) {}
obtener(): T { return this.valor; }
establecer(v: T): void { this.valor = v; }
}
const cajaPerro = new Caja(new Perro());
// const cajaAnimal: Caja<Animal> = cajaPerro; // Error: Caja es invariante
// Si fuera permitido, cajaAnimal.establecer(new Animal()) corrompería cajaPerro
Modificadores in y out de TypeScript 4.7
TypeScript 4.7 introdujo los modificadores explícitos in y out para anotar la varianza de los parámetros de tipo en interfaces y tipos genéricos:
// out: solo se usa en posiciones de salida (covariante):
interface Productor<out T> {
producir(): T;
// establecer(v: T): void; // Error: T en posición de entrada
}
// in: solo se usa en posiciones de entrada (contravariante):
interface Consumidor<in T> {
consumir(v: T): void;
// obtener(): T; // Error: T en posición de salida
}
// Sin anotación: TypeScript calcula la varianza automáticamente.
// Con anotación: TypeScript verifica que la varianza es correcta y puede
// optimizar las comprobaciones de asignabilidad en tipos complejos.
Imagen: Pexels / César Gaviria
