Der Autor des Artikels, den wir heute übersetzen, sagt, TypeScript sei großartig. Als er anfing, TS zu benutzen, mochte er die Freiheit, die dieser Sprache innewohnt. Je mehr Aufwand ein Programmierer in seine Arbeit mit TS-spezifischen Mechanismen steckt, desto größer sind die Vorteile, die er erhält. Dann verwendete er nur periodisch Typanmerkungen. Manchmal nutzte er die Möglichkeiten zur Code-Vervollständigung und zu Compiler-Hinweisen, stützte sich jedoch hauptsächlich auf seine eigene Vision der von ihm gelösten Aufgaben.
Im Laufe der Zeit stellte der Autor dieses Materials fest, dass er jedes Mal, wenn er Fehler umgeht, die bei der Kompilierung festgestellt wurden, eine Zeitbombe in seinen Code einfügt, die während der Programmausführung explodieren kann. Jedes Mal, wenn er mit einem einfachen Konstrukt mit Fehlern „kämpfte“, musste er dafür mit vielen Stunden hartem Debuggen bezahlen.

Infolgedessen kam er zu dem Schluss, dass es besser ist, dies nicht zu tun. Er freundete sich mit dem Compiler an und begann, auf seine Hinweise zu achten. Der Compiler findet Probleme im Code und meldet sie, lange bevor sie echten Schaden anrichten können. Der Autor des Artikels, der sich selbst als Entwickler betrachtete, erkannte, dass der Compiler sein bester Freund ist, da er ihn vor sich selbst schützt. Wie kann man sich nicht an die Worte von Albus Dumbledore erinnern: "Es erfordert viel Mut, sich Ihren Feinden zu widersetzen, aber nicht weniger, als sich Ihren Freunden zu widersetzen."
Egal wie gut der Compiler sein mag, es ist nicht immer einfach zu gefallen. Manchmal ist es sehr schwierig, die Verwendung eines
any
Typs zu vermeiden. Und manchmal scheint es, dass
any
die einzig vernünftige Lösung für ein Problem ist.
Dieses Material konzentriert sich auf zwei Situationen. Indem Sie die Verwendung eines
any
Typs vermeiden, können Sie die Typensicherheit des Codes gewährleisten, die Möglichkeiten für dessen Wiederverwendung eröffnen und ihn intuitiv gestalten.
Generika
Angenommen, wir arbeiten an einer Datenbank einer Schule. Wir haben eine sehr praktische
getBy
. Um das Objekt zu erhalten, das den Schüler mit seinem Namen darstellt, können wir einen Befehl der Form
getBy(model, "name", "Harry")
. Werfen wir einen Blick auf die Implementierung dieses Mechanismus (um den Code nicht zu komplizieren, wird die Datenbank durch ein gewöhnliches Array dargestellt).
type Student = { name: string; age: number; hasScar: boolean; }; const students: Student[] = [ { name: "Harry", age: 17, hasScar: true }, { name: "Ron", age: 17, hasScar: false }, { name: "Hermione", age: 16, hasScar: false } ]; function getBy(model, prop, value) { return model.filter(item => item[prop] === value)[0] }
Wie Sie sehen können, haben wir eine gute Funktion, aber sie verwendet keine Typanmerkungen, und ihre Abwesenheit bedeutet auch, dass eine solche Funktion nicht als typsicher bezeichnet werden kann. Repariere es.
function getBy(model: Student[], prop: string, value): Student | null { return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "name", "Hermione") // result: Student
Unsere Funktion sieht also schon viel besser aus. Der Compiler kennt jetzt die Art des von ihm erwarteten Ergebnisses, dies wird später nützlich sein. Um jedoch ein sicheres Arbeiten mit Typen zu erreichen, haben wir die Möglichkeiten der Wiederverwendung der Funktion geopfert. Was ist, wenn wir es jemals brauchen, um andere Entitäten zu bekommen? Es kann nicht sein, dass diese Funktion in keiner Weise verbessert werden konnte. Und das ist es wirklich.
In TypeScript können wir wie in anderen stark typisierten Sprachen Generika verwenden, die auch als "generische Typen", "universelle Typen" und "Verallgemeinerungen" bezeichnet werden.
Ein Generikum ähnelt einer regulären Variablen, enthält jedoch anstelle eines Werts eine Typdefinition. Wir schreiben den Code unserer Funktion so um, dass anstelle des
Student
Typs der universelle Typ
T
function getBy<T>(model: T[], prop: string, value): T | null { return model.filter(item => item[prop] === value)[0] } const result = getBy<Student>(students, "name", "Hermione") // result: Student
Schönheit! Jetzt ist die Funktion ideal für die Wiederverwendung, während die Typensicherheit noch auf unserer Seite steht. Beachten Sie, wie der
Student
Typ in der letzten Zeile des obigen Code-Snippets, in dem das generische
T
wird, explizit festgelegt
T
. Dies geschieht, um das Beispiel so klar wie möglich zu gestalten. Der Compiler kann jedoch unabhängig den erforderlichen Typ ableiten. In den folgenden Beispielen werden solche Typverfeinerungen daher nicht durchgeführt.
Jetzt haben wir eine zuverlässige Hilfsfunktion, die zur Wiederverwendung geeignet ist. Es kann jedoch noch verbessert werden. Was ist, wenn bei der Eingabe des zweiten Parameters ein Fehler auftritt und anstelle von
"name"
"naem"
? Die Funktion verhält sich so, als ob sich der gesuchte Schüler einfach nicht in der Datenbank befindet und, was am unangenehmsten ist, keine Fehler erzeugt. Dies kann zu einem langfristigen Debugging führen.
Zum Schutz vor solchen Fehlern führen wir einen anderen universellen Typ ein,
P
In diesem Fall ist es erforderlich, dass
P
ein Schlüssel vom Typ
T
ist. Wenn hier
Student
verwendet wird, ist es daher erforderlich, dass
P
die Zeichenfolge
"name"
,
"age"
oder
"hasScar"
. Hier erfahren Sie, wie es geht.
function getBy<T, P extends keyof T>(model: T[], prop: P, value): T | null { return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "naem", "Hermione") // Error: Argument of type '"naem"' is not assignable to parameter of type '"name" | "age" | "hasScar"'.
Die Verwendung von Generika und dem
keyof
ist ein sehr mächtiger Trick. Wenn Sie Programme in eine IDE schreiben, die TypeScript unterstützt, können Sie durch Eingabe von Argumenten die Funktionen zur automatischen Vervollständigung nutzen, was sehr praktisch ist.
Wir haben die Arbeit an der
getBy
Funktion jedoch noch nicht abgeschlossen. Sie hat ein drittes Argument, dessen Art wir noch nicht festgelegt haben. Das passt uns überhaupt nicht. Bisher konnten wir nicht im Voraus wissen, um welchen Typ es sich handeln sollte, da dies davon abhängt, was wir als zweites Argument übergeben. Da wir nun Typ
P
, können wir den Typ für das dritte Argument dynamisch ableiten. Der Typ des dritten Arguments ist letztendlich
T[P]
. Wenn
T
Student
und
P
"age"
, hat
T[P]
die Typennummer.
function getBy<T, P extends keyof T>(model: T[], prop: P, value: T[P]): T | null { return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "age", "17") // Error: Argument of type '"17"' is not assignable to parameter of type 'number'. const anotherResult = getBy(students, "hasScar", "true") // Error: Argument of type '"true"' is not assignable to parameter of type 'boolean'. const yetAnotherResult = getBy(students, "name", "Harry") //
Ich hoffe, dass Sie jetzt ein absolut klares Verständnis für die Verwendung von Generika in TypeScript haben. Wenn Sie jedoch mit allem, was Sie mit dem hier beschriebenen Code experimentieren möchten, sehr gut experimentieren möchten, können Sie hier einen Blick darauf werfen.
Bestehende Typen erweitern
Manchmal müssen wir Daten oder Funktionen zu Schnittstellen hinzufügen, deren Code wir nicht ändern können. Möglicherweise müssen Sie das Standardobjekt ändern, z. B. dem
window
eine Eigenschaft hinzufügen oder das Verhalten einer externen Bibliothek wie
Express
. In beiden Fällen können Sie das Objekt, mit dem Sie arbeiten möchten, nicht direkt beeinflussen.
Wir werden eine Lösung für dieses Problem suchen, indem wir die bereits bekannte Funktion
getBy
zum
Array
Prototyp hinzufügen. Auf diese Weise können wir mit dieser Funktion genauere syntaktische Konstruktionen erstellen. Im Moment sprechen wir nicht darüber, ob es gut oder schlecht ist, Standardobjekte zu erweitern, da unser Hauptziel darin besteht, den betrachteten Ansatz zu untersuchen.
Wenn wir versuchen, dem
Array
Prototyp eine Funktion hinzuzufügen, wird dem Compiler dies nicht sehr gefallen:
Array.prototype.getBy = function <T, P extends keyof T>( this: T[], prop: P, value: T[P] ): T | null { return this.filter(item => item[prop] === value)[0] || null; }; // Error: Property 'getBy' does not exist on type 'any[]'. const bestie = students.getBy("name", "Ron"); // Error: Property 'getBy' does not exist on type 'Student[]'. const potionsTeacher = (teachers as any).getBy("subject", "Potions") // ... ?
Wenn wir versuchen, den Compiler zu beruhigen, indem wir das Konstrukt regelmäßig
as any
Konstrukt verwenden, werden wir alles, was wir erreicht haben, aufheben. Der Compiler ist stumm, aber Sie können die sichere Arbeit mit Typen vergessen.
Es wäre besser, den
Array
Typ zu erweitern, aber bevor wir dies tun, wollen wir uns damit befassen, wie TypeScript Situationen behandelt, in denen zwei Schnittstellen desselben Typs im Code vorhanden sind. Hier wird ein einfaches Aktionsschema angewendet. Anzeigen werden nach Möglichkeit kombiniert. Wenn Sie sie nicht kombinieren können, gibt das System einen Fehler aus.
Dieser Code funktioniert also:
interface Wand { length: number } interface Wand { core: string } const myWand: Wand = { length: 11, core: "phoenix feather" }
Und dieser ist nicht:
interface Wand { length: number } interface Wand { length: string }
Nachdem wir uns damit befasst haben, sehen wir, dass wir vor einer ziemlich einfachen Aufgabe stehen. Wir müssen
getBy
die
Array<T>
-Schnittstelle deklarieren und die Funktion
getBy
hinzufügen.
interface Array<T> { getBy<P extends keyof T>(prop: P, value: T[P]): T | null; } Array.prototype.getBy = function <T, P extends keyof T>( this: T[], prop: P, value: T[P] ): T | null { return this.filter(item => item[prop] === value)[0] || null; }; const bestie = students.getBy("name", "Ron"); // ! const potionsTeacher = (teachers as any).getBy("subject", "Potions") //
Beachten Sie, dass der größte Teil des Codes, den Sie wahrscheinlich in Moduldateien schreiben, erforderlich ist. Um Änderungen an der
Array
Schnittstelle vorzunehmen, benötigen Sie Zugriff auf den globalen Bereich. Sie können dies tun, indem Sie die Typdefinition in
declare global
. Zum Beispiel so:
declare global { interface Array<T> { getBy<P extends keyof T>(prop: P, value: T[P]): T | null; } }
Wenn Sie die Schnittstelle einer externen Bibliothek erweitern möchten, benötigen Sie höchstwahrscheinlich Zugriff auf den
namespace
dieser Bibliothek. Hier ist ein Beispiel für das Hinzufügen des
userId
zu
Request
aus der
Express
Bibliothek:
declare global { namespace Express { interface Request { userId: string; } } }
Sie können hier mit dem Code in diesem Abschnitt experimentieren.
Zusammenfassung
In diesem Artikel haben wir uns mit Techniken zur Verwendung von Generika und Typerweiterungen in TypeScript befasst. Wir hoffen, dass das, was Sie heute gelernt haben, Ihnen hilft, zuverlässigen, verständlichen und typsicheren Code zu schreiben.
Liebe Leser! Wie stehen Sie zu einem Typ in TypeScript?
