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
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
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
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
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
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
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
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
Weitere ErweiterungenEmpfehlung:Berücksichtigen Sie Kovarianz und Kontravarianz beim Entwurf generischer APIs, um sichereren Typcode zu schreiben.