TypeScript covarianza y contravarianza

La covarianza y la contravarianza son conceptos importantes en el sistema de tipos de TypeScript; comprenderlos ayuda a escribir código seguro en cuanto a tipos.

Describen el comportamiento de los tipos genéricos en las relaciones entre tipos padre e hijo.


Diagrama SVG: covarianza y contravarianza Fondo Título Conceptos de covarianza y contravarianza Covarianza Covarianza (Covariante) Tipo de salida: seguro Dog → Animal Provider<Dog> → Provider<Animal> Invarianza Invarianza (Invariante) Tipo de entrada/salida: estricto No se pueden asignar entre sí Consumer<Dog> ≠ Consumer<Animal> Contravarianza Contravarianza (Contravariante) Tipo de entrada: invertido Animal → Dog Consumer<Animal> → Consumer<Dog> Parte inferior: reglas Comportamiento predeterminado de TypeScript Regla 1 Valor de retorno: covariante Regla 2 Parámetros: contravariante (strictFunctionTypes) Regla 3 Propiedades: covariante Flecha

¿Por qué se necesitan la covarianza y la contravarianza?

Cuando usamos clases o funciones genéricas, el comportamiento de los parámetros de tipo no es tan simple como podrías imaginar.

Asignar Dog a Animal es seguro, pero asignar una función que maneja Animal a una función que maneja Dog puede no ser seguro.

Las reglas de covarianza y contravarianza ayudan a TypeScript a detectar estos posibles errores de tipo.

Conceptos:La covarianza permite la conversión de subtipos a supertipos, la contravarianza permite la conversión de supertipos a subtipos, y la invarianza no permite la conversión en ninguna dirección.


Covarianza (Covariante)

La covarianza significa que un subtipo puede asignarse a un supertipo. Para los tipos de salida (como los valores de retorno de funciones), esto es seguro.

Ejemplo

// Definir los tipos de animal y perro
class Animal {
    name: string = "Animal";
}

class Dog extends Animal {
    breed: string = "Perro criollo";
}

// Covarianza: el tipo de salida puede convertirse a un tipo más general
// Una función que devuelve Dog puede asignarse a una función que devuelve Animal
type AnimalGetter = () => Animal;
type DogGetter = () => Dog;

// Dog es una subclase de Animal, por lo que DogGetter puede asignarse a AnimalGetter
const getDog: DogGetter = () => new Dog();
const getAnimal: AnimalGetter = getDog;  // Covarianza: seguro

// Ejecutar
const animal: Animal = getAnimal();
console.log("Nombre del animal: " + animal.name);

Resultado de la ejecución:

Nombre del animal: animal

Covarianza del valor de retorno:Es seguro que una función devuelva un subtipo más específico, porque el objeto devuelto cumple necesariamente con los requisitos del supertipo.


Contravarianza (Contravariante)

La contravarianza significa que, para los tipos de entrada (como los parámetros de función), un supertipo puede asignarse a un subtipo.

Ejemplo

// Definir tipos
class Animal {
    name: string = "Animal";
}

class Dog extends Animal {
    breed: string = "Perro criollo";
}

// Contravarianza: el tipo de entrada puede convertirse a un tipo más específico
// Una función que acepta Animal puede asignarse a una función que acepta Dog
type DogConsumer = (dog: Dog) => void;
type AnimalConsumer = (animal: Animal) => void;

// Si una función que acepta un tipo más general puede asignarse a una función que acepta un tipo más específico
// Entonces, al pasar Dog, la función podría no manejarlo (carece de las propiedades específicas de Dog)
const consumeAnimal: AnimalConsumer = (animal) => {
    console.log("Procesando animal: " + animal.name);
};

const consumeDog: DogConsumer = consumeAnimal;  // Contravarianza: seguro

// Ejecutar
const dog = new Dog();
dog.breed = "Husky";
consumeDog(dog);

Contravarianza de parámetros:Los parámetros de función usan contravarianza porque una función que acepta un tipo más específico no puede manejar un tipo más general.


Habilitar tipos de función estrictos

TypeScript realiza por defecto la comprobación de contravarianza en los parámetros de función. Habilitar strictFunctionTypes aplica esta regla de forma estricta.

Ejemplo

// Definir tipos
interface Animal {
    readonly name: string;
}

interface Dog extends Animal {
    readonly breed: string;
}

// Definir tipos de función
type GetName = (animal: Animal) => string;
type GetDogBreed = (dog: Dog) => string;

// Asignación correcta
const getDogBreed: GetDogBreed = (dog) => dog.breed;

// Intento de asignación: dará error con strictFunctionTypes
// Porque AnimalConsumer (parámetro más general) no puede asignarse a DogConsumer (parámetro más específico)
// Esto se debe a que los parámetros son contravariantes

function printAnimalName(animal: Animal): string {
    return animal.name;
}

// Intentar asignar una función que acepta un tipo más general a un tipo más específico
// const getSpecific: GetDogBreed = printAnimalName; // ¡Error!

console.log("Raza de perro: " + getDogBreed({ name: "Wangcai", breed: "Husky" }));

strictFunctionTypes:Habilitar esta opción en tsconfig.json permite una comprobación de tipos más estricta.


Covarianza de clases genéricas

Las propiedades de las clases genéricas son covariantes por defecto.

Ejemplo

// Definir tipos
class Animal {
    name: string = "Animal";
}

class Dog extends Animal {
    breed: string = "Perro";
}

// Clase contenedora genérica
class Cage<T> {
    animal: T;

    constructor(animal: T) {
        this.animal = animal;
    }
}

// Covarianza: se puede asignar un contenedor de subtipo a un contenedor de supertipo
const dogCage = new Cage(new Dog());
const animalCage: Cage<Animal> = dogCage;  // Covarianza: seguro

// animalCage ahora puede usarse de forma segura como una jaula que contiene animales
console.log("Nombre del animal: " + animalCage.animal.name);

Covarianza de propiedades:Las propiedades de los objetos son covariantes; las propiedades de subtipo pueden asignarse a propiedades de supertipo.


Covarianza de arrays

En TypeScript, los arrays son covariantes, pero hay que tener en cuenta los problemas que causa la mutabilidad.

Ejemplo

// Definir tipos
class Animal {
    name: string = "Animal";
}

class Dog extends Animal {
    breed: string = "Perro";
}

// Covarianza de arrays
const dogs: Dog[] = [
    { name: "Wangcai", breed: "Husky" },
    { name: "Xiaobai", breed: "Samoyedo" }
];

// Dog[] puede asignarse a Animal[]
const animals: Animal[] = dogs;  // Covarianza: seguro

// Problema: aunque es seguro a nivel de tipos, en realidad se pueden añadir otros animales
// animals.push({ name: "Gato", breed: "Felino" }); // ¡Puede causar problemas en tiempo de ejecución!

console.log("Número de animales: " + animals.length);

Mutabilidad de arrays:Modificar un array después de una asignación covariante puede provocar errores en tiempo de ejecución; hay que tener cuidado.


Uso de extends para asignación segura

Después de comprender la covarianza y la contravarianza, se pueden diseñar interfaces genéricas de forma segura.

Ejemplo

// Definir tipos
interface Producer<T> {
    // Método productor: el valor de retorno es covariante
    produce(): T;
}

interface Consumer<T> {
    // Método consumidor: los parámetros son contravariantes
    consume(value: T): void;
}

// Implementación concreta
class DogProducer implements Producer<Dog> {
    produce(): Dog {
        return { name: "Wangcai", breed: "Husky" };
    }
}

class AnimalConsumer implements Consumer<Animal> {
    consume(animal: Animal): void {
        console.log("Consumiendo animal: " + animal.name);
    }
}

// Producer<Dog> puede asignarse a Producer<Animal> (covariante)
const animalProducer: Producer<Animal> = new DogProducer();

// Consumer<Animal> puede asignarse a Consumer<Dog> (contravariante)
const dogConsumer: Consumer<Dog> = new AnimalConsumer();

// Prueba
const animal = animalProducer.produce();
console.log("Produciendo: " + animal.name);

dogConsumer.consume({ name: "Wangcai", breed: "Husky" });

Principios de diseño:Elige la dirección de tipo adecuada según el propósito del método para mejorar la seguridad de tipos de la API.


Notas importantes

  • Covarianza del valor de retorno:Es seguro que una función devuelva un subtipo
  • Contravarianza de parámetros:Es seguro que los parámetros de función usen un supertipo
  • Habilitar modo estricto:Usa strictFunctionTypes para obtener una comprobación más estricta
  • Covarianza de arrays:Ten en cuenta los posibles problemas que causa la mutabilidad

Buenas prácticas:Comprender la covarianza y la contravarianza ayuda a diseñar APIs más seguras en cuanto a tipos y evitar errores en tiempo de ejecución.


Resumen

La covarianza y la contravarianza son conceptos fundamentales del sistema de tipos de TypeScript.

  • Covarianza:Subtipo → supertipo, para tipos de salida
  • Contravarianza:Tipo padre → tipo hijo, para tipos de entrada
  • Invarianza:No se pueden asignar entre sí
  • strictFunctionTypes:Activar la verificación estricta de tipos de función

Recomendación:Al diseñar API genéricas, considere la covarianza y la contravarianza para escribir código de tipos más seguro.

Otras extensiones