Kovarianz und Kontravarianz in TypeScript

Kovarianz und Kontravarianz sind wichtige Konzepte im Typsystem von TypeScript; ihr Verständnis hilft Ihnen, typsicheren Code zu schreiben.

Sie beschreiben, wie sich generische Typen in Eltern-Kind-Typbeziehungen verhalten.


SVG-Diagramm: Kovarianz und Kontravarianz Background Title Covariance and Contravariance Concepts Covariant Covariant Output type: safe Dog → Animal Provider<Dog> → Provider<Animal> Invariant Invariant Input/output types: strict Cannot be assigned to each other Consumer<Dog> ≠ Consumer<Animal> Contravariant Contravariant Input type: reversed Animal → Dog Consumer<Animal> → Consumer<Dog> Bottom section: rules TypeScript default behavior Rule 1 Return type: covariant Rule 2 Parameters: contravariant (strictFunctionTypes) Rule 3 Properties: covariant Arrow markers

Warum brauchen wir Kovarianz und Kontravarianz?

Bei der Verwendung generischer Klassen oder Funktionen ist das Verhalten von Typparametern nicht so einfach, wie Sie vielleicht denken.

Das Zuweisen von Dog zu Animal ist sicher, aber das Zuweisen einer Funktion, die Animal verarbeitet, zu einer Funktion, die Dog verarbeitet, ist möglicherweise nicht sicher.

Die Regeln für Kovarianz und Kontravarianz helfen TypeScript, diese potenziellen Typfehler zu erkennen.

Konzepte:Kovarianz erlaubt die Umwandlung von Untertypen in Obertypen, Kontravarianz erlaubt die Umwandlung von Obertypen in Untertypen, und Invarianz erlaubt keine Umwandlung in beide Richtungen.


Kovariant

Kovarianz bedeutet, dass ein Untertyp einem Obertyp zugewiesen werden kann. Dies ist sicher für Ausgabetypen (wie Rückgabewerte von Funktionen).

Beispiel

// Definiere die Typen Animal und Dog
class Animal {
    name: string = "Animal";
}

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

// Kovarianz: Ausgabetypen können in breitere Typen umgewandelt werden
// Eine Funktion, die Dog zurückgibt, kann einer Funktion zugewiesen werden, die Animal zurückgibt
type AnimalGetter = () => Animal;
type DogGetter = () => Dog;

// Dog ist ein Subtyp von Animal, daher kann DogGetter AnimalGetter zugewiesen werden
const getDog: DogGetter = () => new Dog();
const getAnimal: AnimalGetter = getDog;  // Kovarianz: sicher

// Ausführen
const animal: Animal = getAnimal();
console.log("Tiername: " + animal.name);

Ausgabe:

Animal Name: Animal

Rückgabetyp-Kovarianz:Es ist sicher für eine Funktion, einen spezifischeren Untertyp zurückzugeben, weil das zurückgegebene Objekt immer die Anforderungen des Obertyps erfüllt.


Kontravariant

Kontravarianz bedeutet, dass für Eingabetypen (wie Funktionsparameter) ein Obertyp einem Untertyp zugewiesen werden kann.

Beispiel

// Typen definieren
class Animal {
    name: string = "Animal";
}

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

// Kontravarianz: Eingabetypen können in spezifischere Typen konvertiert werden
// Eine Funktion, die Animal akzeptiert, kann einer Funktion zugewiesen werden, die Dog akzeptiert
type DogConsumer = (dog: Dog) => void;
type AnimalConsumer = (animal: Animal) => void;

// Wenn eine Funktion, die einen breiteren Typ akzeptiert, einer Funktion zugewiesen werden kann, die einen spezifischeren Typ akzeptiert
// dann kann die Funktion, wenn wir einen Dog übergeben, möglicherweise nicht damit umgehen (ihr fehlen Dog-spezifische Eigenschaften)
const consumeAnimal: AnimalConsumer = (animal) => {
    console.log("Tier wird verarbeitet: " + animal.name);
};

const consumeDog: DogConsumer = consumeAnimal;  // Kontravarianz: sicher

// Ausführen
const dog = new Dog();
dog.breed = "Husky";
consumeDog(dog);

Parameterkontravarianz:Funktionsparameter verwenden Kontravarianz, weil eine Funktion, die einen spezifischeren Typ akzeptiert, keinen breiteren Typ verarbeiten kann.


Strikte Funktionstypen aktivieren

TypeScript prüft Funktionsparameter standardmäßig kontravariant. Das Aktivieren von strictFunctionTypes erzwingt diese Regel.

Beispiel

// Typen definieren
interface Animal {
    readonly name: string;
}

interface Dog extends Animal {
    readonly breed: string;
}

// Funktionstypen definieren
type GetName = (animal: Animal) => string;
type GetDogBreed = (dog: Dog) => string;

// Korrekte Zuweisung
const getDogBreed: GetDogBreed = (dog) => dog.breed;

// Versuch der Zuweisung - führt unter strictFunctionTypes zu einem Fehler
// Weil AnimalConsumer (breiterer Parameter) nicht DogConsumer (spezifischerer Parameter) zugewiesen werden kann
// Das liegt daran, dass Parameter kontravariant sind

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

// Versuch, eine Funktion, die einen breiteren Typ akzeptiert, einem spezifischeren Typ zuzuweisen
// const getSpecific: GetDogBreed = printAnimalName; // Fehler!

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

strictFunctionTypes:Aktivieren Sie diese Option in tsconfig.json für eine strengere Typprüfung.


Kovarianz generischer Klassen

Eigenschaften generischer Klassen sind standardmäßig kovariant.

Beispiel

// Typen definieren
class Animal {
    name: string = "Animal";
}

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

// Generische Containerklasse
class Cage<T> {
    animal: T;

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

// Kovarianz: Ein Subtyp-Container kann einem Supertyp-Container zugewiesen werden
const dogCage = new Cage(new Dog());
const animalCage: Cage<Animal> = dogCage;  // Kovarianz: sicher

// animalCage kann jetzt sicher als Käfig verwendet werden, der Tiere enthält
console.log("Tiername: " + animalCage.animal.name);

Kovarianz von Eigenschaften:Objekt-Eigenschaften sind kovariant; eine Untertyp-Eigenschaft kann einer Obertyp-Eigenschaft zugewiesen werden.


Array-Kovarianz

Arrays in TypeScript sind kovariant, aber man muss sich der Probleme bewusst sein, die durch Veränderbarkeit entstehen.

Beispiel

// Typen definieren
class Animal {
    name: string = "Animal";
}

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

// Array-Kovarianz
const dogs: Dog[] = [
    { name: "Wangcai", breed: "Husky" },
    { name: "Xiao Bai", breed: "Samoyed" }
];

// Dog[] kann Animal[] zugewiesen werden
const animals: Animal[] = dogs;  // Kovarianz: sicher

// Problem: Obwohl es typsicher ist, kann man tatsächlich andere Tiere hinzufügen
// animals.push({ name: "Cat", breed: "Cat" }); // Kann zur Laufzeit Probleme verursachen!

console.log("Anzahl der Tiere: " + animals.length);

Array-Mutabilität:Das Ändern eines Arrays nach einer kovarianten Zuweisung kann Laufzeitfehler verursachen; Vorsicht.


Verwendung von extends für sichere Zuweisung

Nachdem du Kovarianz und Kontravarianz verstanden hast, kannst du generische Schnittstellen sicher entwerfen.

Beispiel

// Typen definieren
interface Producer<T> {
    // Producer-Methode: Rückgabewert ist kovariant
    produce(): T;
}

interface Consumer<T> {
    // Consumer-Methode: Parameter sind kontravariant
    consume(value: T): void;
}

// Konkrete Implementierung
class DogProducer implements Producer<Dog> {
    produce(): Dog {
        return { name: "Wangcai", breed: "Husky" };
    }
}

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

// Producer<Dog> kann Producer<Animal> zugewiesen werden (kovariant)
const animalProducer: Producer<Animal> = new DogProducer();

// Consumer<Animal> kann Consumer<Dog> zugewiesen werden (kontravariant)
const dogConsumer: Consumer<Dog> = new AnimalConsumer();

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

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

Entwurfsprinzipien:Wählen Sie die passende Typrichtung basierend auf dem Zweck der Methode, um die Typsicherheit Ihrer API zu verbessern.


Wichtige Hinweise

  • Kovarianz des Rückgabetyps:Für Funktionen ist es sicher, einen Subtyp zurückzugeben
  • Parameterkontravarianz:Für Funktionsparameter ist es sicher, einen Supertyp zu verwenden
  • Strict-Modus aktivieren:Verwenden Sie strictFunctionTypes für eine strengere Überprüfung
  • Array-Kovarianz:Achten Sie auf potenzielle Probleme, die durch Veränderbarkeit verursacht werden

Bewährte Methoden:Das Verständnis von Kovarianz und Kontravarianz hilft dabei, typsicherere APIs zu entwerfen und Laufzeitfehler zu vermeiden.


Zusammenfassung

Kovarianz und Kontravarianz sind Kernkonzepte des TypeScript-Typsystems.

  • Kovarianz:Subtyp → Supertyp, verwendet für Ausgabetypen
  • Kontravarianz:Elterntyp → Kindtyp, verwendet für Eingabetypen
  • Invarianz:Können nicht gegenseitig zugewiesen werden
  • strictFunctionTypes:Strikte Funktions-Typprüfung aktivieren

Empfehlung:Berücksichtigen Sie Kovarianz und Kontravarianz beim Entwurf generischer APIs, um sichereren Typcode zu schreiben.

Weitere Erweiterungen