
Als ich kürzlich die Gründe für den fehlerhaften Betrieb meines Heimprojekts untersuchte, bemerkte ich erneut einen Fehler, der sich aufgrund von Müdigkeit häufig wiederholt. Der Kern des Fehlers besteht darin, dass ich beim Aufrufen einer Funktion mehrere Bezeichner in einem Codeblock habe und den Bezeichner eines Objekts eines anderen Typs übergebe. In diesem Artikel werde ich darüber sprechen, wie dieses Problem mit TypeScript gelöst werden kann.
Ein bisschen Theorie
TypeScript basiert auf struktureller Typisierung, die gut zur Entenideologie von JavaScript passt. Eine ausreichende Anzahl von Artikeln wurde darüber geschrieben. Ich werde sie nicht wiederholen, sondern nur den Hauptunterschied zur nominativen Typisierung skizzieren, die in anderen Sprachen häufiger vorkommt. Schauen wir uns ein kleines Beispiel an.
class Car { id: number; numberOfWheels: number; move (x: number, y: number) {
Warum verhält sich TypeScript so? Dies ist nur eine Manifestation der strukturellen Typisierung. Im Gegensatz zum Nominativ, das Typnamen überwacht, entscheidet die strukturelle Typisierung anhand ihres Inhalts über die Kompatibilität von Typen. Die Car-Klasse enthält alle Eigenschaften und Methoden der Boat-Klasse, sodass Car als Boot verwendet werden kann. Das Gegenteil ist nicht der Fall, da Boat nicht über die Eigenschaft numberOfWheels verfügt.
Bezeichner eingeben
Zunächst legen wir Typen für Bezeichner fest
type CarId: number; type BoatId: number;
und schreiben Sie die Klassen mit diesen Typen neu.
class Car { id: CarId; numberOfWheels: number; move (x: number, y: number) {
Sie werden feststellen, dass sich die Situation nicht wesentlich geändert hat, da wir immer noch keine Kontrolle darüber haben, woher wir die Kennung haben, und Sie werden Recht haben. Dieses Beispiel bietet jedoch bereits einige Vorteile.
Während der Programmentwicklung kann sich der Typ der Kennung plötzlich ändern. So kann beispielsweise eine bestimmte Fahrzeugnummer, die für das Projekt eindeutig ist, durch eine Zeichenfolgen-VIN-Nummer ersetzt werden. Ohne Angabe des Typs des Bezeichners müssen Sie die Zahl an allen Stellen, an denen sie auftritt, durch eine Zeichenfolge ersetzen. Bei der Typaufgabe muss die Änderung nur an einer Stelle vorgenommen werden, an der der Typ selbst bestimmt wird.
Beim Aufrufen von Funktionen erhalten wir von unserem Code-Editor Hinweise, welche Typbezeichner sein sollten. Angenommen, wir haben die folgenden Funktionen deklariert:
function getCarById(id: CarId): Car {
Dann erhalten wir vom Editor einen Hinweis, dass wir nicht nur eine Nummer, sondern auch CarId oder BoatId übertragen müssen.
Emulieren Sie die strengste Eingabe
In TypeScript gibt es keine nominelle Typisierung, aber wir können sein Verhalten emulieren und jeden Typ eindeutig machen. Fügen Sie dazu dem Typ eine eindeutige Eigenschaft hinzu. Dieser Trick wird in den englischsprachigen Artikeln unter dem Begriff Branding erwähnt. So sieht er aus:
type BoatId = number & { _type: 'BoatId'}; type CarId = number & { _type: 'CarId'};
Nachdem wir darauf hingewiesen haben, dass unsere Typen sowohl eine Zahl als auch ein Objekt mit einer Eigenschaft mit einem eindeutigen Wert sein müssen, haben wir unsere Typen für das Verständnis der strukturellen Typisierung inkompatibel gemacht. Mal sehen, wie es funktioniert.
let carId: CarId; let boatId: BoatId; let car: Car; let boat: Boat; car = getCarById(carId);
Bis auf die letzten vier Zeilen sieht alles gut aus. Um Bezeichner zu erstellen, benötigen Sie eine Hilfsfunktion:
function makeCarIdFromVin(id: number): CarId { return vin as any; }
Der Nachteil dieser Methode ist, dass diese Funktion zur Laufzeit verbleibt.
Starkes Tippen etwas weniger streng machen
Im letzten Beispiel musste ich eine zusätzliche Funktion verwenden, um den Bezeichner zu erstellen. Sie können es mithilfe der Flavor-Schnittstellendefinition entfernen:
interface Flavoring<FlavorT> { _type?: FlavorT; } export type Flavor<T, FlavorT> = T & Flavoring<FlavorT>;
Jetzt können Sie Typen für Bezeichner wie folgt festlegen:
type CarId = Flavor<number, “CarId”> type BoatId = Flavor<number, “BoatId”>
Da die Eigenschaft _type optional ist, können Sie eine implizite Konvertierung verwenden:
let boatId: BoatId = 5; // OK let carId: CarId = 3; // OK
Und wir können die Bezeichner immer noch nicht verwechseln:
let carId: CarId = boatId; // ERROR
Welche Option zu wählen
Beide Optionen haben ein Existenzrecht. Branding hat den Vorteil, eine Variable vor direkter Zuordnung zu schützen. Dies ist nützlich, wenn die Variable die Zeichenfolge in einem bestimmten Format speichert, z. B. einem absoluten Dateipfad, einem Datum oder einer IP-Adresse. Die Hilfsfunktion, die sich in diesem Fall mit der Typkonvertierung befasst, kann auch Eingabedaten prüfen und verarbeiten. In anderen Fällen ist es bequemer, Flavor zu verwenden.
Quellen
- Ausgangspunkt stackoverflow.com
- Freie Interpretation des Artikels