Eine Schnittstelle ist eine Form eines Objekts. Ein Standard-JavaScript-Objekt ist eine Abbildung von key:value-Paaren. JavaScript-Objektschlüssel sind in fast allen Fällen Strings und ihre Werte sind alle unterstützten JavaScript-Werte (primitiv oder abstrakt).

Eine Schnittstelle informiert den TypeScript-Compiler über Eigenschaftsnamen, die ein Objekt haben kann, und ihre entsprechenden Werttypen. Daher ist eine Schnittstelle ein Typ und ein abstrakter Typ, da sie aus primitiven Typen besteht.

Wenn wir ein Objekt mit Eigenschaften (Schlüsseln) und Werten definieren, erstellt TypeScript eine implizite Schnittstelle, indem es die Eigenschaftsnamen und den Datentyp ihrer Werte im Objekt betrachtet. Dies geschieht aufgrund der Typinferenz.

(object-shape.ts)

Im obigen Beispiel haben wir ein Objekt student mit den Feldern firstName, lastName, age und getSalary erstellt und einige Anfangswerte zugeordnet. Mit diesen Informationen erstellt TypeScript einen impliziten Schnittstellentyp für student.

{
firstName: string;
lastName: string;
age: number;
getSalary: (base: number) => number;
}

Eine Schnittstelle ist wie ein Objekt, enthält aber nur die Informationen über die Objekteigenschaften und ihre Typen. Wir können auch einen Schnittstellentyp erstellen und ihm einen Namen geben, damit wir ihn verwenden können, um Objektwerte zu annotieren, aber hier hat diese Schnittstelle keinen Namen, da sie implizit erstellt wurde. Sie können dies mit dem Funktionstyp in der vorherigen Lektion vergleichen, der zunächst implizit erstellt wurde und dann haben wir einen Funktionstyp explizit mit Hilfe von Typ-Alias erstellt.

Lassen Sie uns versuchen, die Objekteigenschaften zu verändern, nachdem sie definiert wurden.

(shape-override.ts)

Wie im obigen Beispiel zu sehen ist, merkt sich TypeScript die Form eines Objekts, da der Typ ross die implizite Schnittstelle ist. Wenn wir versuchen, den Wert einer Eigenschaft mit einem Wert eines anderen Typs als dem in der Schnittstelle angegebenen zu überschreiben oder versuchen, eine neue Eigenschaft hinzuzufügen, die nicht in der Schnittstelle angegeben ist, wird der TypeScript-Compiler das Programm nicht kompilieren.

Wenn Sie möchten, dass ein Objekt grundsätzlich eine beliebige Eigenschaft hat, dann können Sie explizit einen Wert any markieren und der TypeScript-Compiler wird den Typ nicht aus dem zugewiesenen Objektwert ableiten. Es gibt andere, bessere Möglichkeiten, genau das zu erreichen, und wir werden sie in diesem Artikel durchgehen.

(shape-override-any.ts)

Schnittstellendeklaration

Die implizite Schnittstelle, die wir bisher gesehen haben, ist zwar technisch gesehen ein Typ, aber sie wurde nicht explizit definiert. Wie besprochen, ist eine Schnittstelle nichts anderes als die Form, die ein Objekt annehmen kann. Wenn Sie eine Funktion haben, die ein Argument akzeptiert, das ein Objekt sein soll, aber eine bestimmte Form hat, dann müssen wir dieses Argument (Parameter) mit einem Schnittstellentyp annotieren.

(Argument-mit-Form.ts)

Im obigen Beispiel haben wir eine Funktion getPersonInfo definiert, die ein Objektargument akzeptiert, das firstName, lastName, age und getSalary Felder mit bestimmten Datentypen hat. Beachten Sie, dass wir ein Objekt, das Eigenschaftsnamen und ihre entsprechenden Typen enthält, als Typ mit der :<type>-Annotation verwendet haben. Dies ist ein Beispiel für eine anonyme Schnittstelle, da die Schnittstelle keinen Namen hat, sondern inline verwendet wurde.

Das alles scheint ein wenig kompliziert zu sein. Wenn das ross-Objekt komplizierter wird und an mehreren Stellen verwendet werden muss, scheint TypeScript eine Sache zu sein, die man anfangs mochte, mit der man aber jetzt nur noch schwer umgehen kann. Um dieses Problem zu lösen, definieren wir einen Schnittstellentyp mit dem Schlüsselwort interface.

(argument-with-interface.ts)

Im obigen Beispiel haben wir eine Schnittstelle Person definiert, die die Form eines Objekts beschreibt, aber dieses Mal haben wir einen Namen, mit dem wir auf diesen Typ verweisen können. Wir haben diesen Typ verwendet, um die Variable ross und das Argument person der Funktion getPersonIfo zu annotieren. Dadurch wird TypeScript angewiesen, diese Entitäten anhand der Form Person zu validieren.

Warum eine Schnittstelle verwenden?

Ein Schnittstellentyp kann wichtig sein, um eine bestimmte Form zu erzwingen. Normalerweise vertrauen wir in JavaScript zur Laufzeit blind darauf, dass ein Objekt immer eine bestimmte Eigenschaft enthält und dass diese Eigenschaft immer einen Wert eines bestimmten Typs hat, wie z. B. {age: 21, ...}.

Wenn wir anfangen, Operationen mit dieser Eigenschaft auszuführen, ohne zuerst zu prüfen, ob diese Eigenschaft auf dem Objekt vorhanden ist oder ob ihr Wert das ist, was wir erwartet haben, können die Dinge schief gehen, und Ihre Anwendung kann danach unbrauchbar werden. Zum Beispiel {age: '21', ...}, hier age ist der Wert ein string.

Schnittstellen bieten einen sicheren Mechanismus, um mit solchen Szenarien zur Kompilierungszeit umzugehen. Wenn Sie versehentlich eine Eigenschaft für ein Objekt verwenden, das nicht existiert, oder den Wert einer Eigenschaft in einer illegalen Operation verwenden, wird der TypeScript-Compiler Ihr Programm nicht kompilieren. Sehen wir uns ein Beispiel an.

(interface-safety.ts)

Im obigen Beispiel versuchen wir, die Eigenschaft name des Arguments _student innerhalb der Funktion printStudent zu verwenden. Da das _student-Argument ein Typ der Student-Schnittstelle ist, gibt der TypeScript-Compiler bei der Kompilierung einen Fehler aus, da diese Eigenschaft in der Student-Schnittstelle nicht existiert.

Gleichermaßen ist 100 — _student.firstName keine gültige Operation, da die firstName-Eigenschaft ein Typ von string ist, und als ich das letzte Mal nachgesehen habe, kann man in JavaScript kein string von einem number subtrahieren (ergibt NaN).

Im obigen Beispiel haben wir die traditionelle Schreibweise des Funktionstyps für das Feld getSalary verwendet. Sie können jedoch auch die Funktionssyntax ohne den Rumpf für dasselbe verwenden, was im Allgemeinen in Schnittstellen verwendet wird.

interface Student {
firstName: string;
lastName: string;
age: number;
getSalary(base: number): number;
};

Optionale Eigenschaften

Manchmal muss ein Objekt eine Eigenschaft haben, die Daten eines bestimmten Datentyps enthält, aber es ist nicht zwingend erforderlich, dass diese Eigenschaft im Objekt vorhanden ist. Dies ist vergleichbar mit den optionalen Funktionsparametern, die wir in der vorherigen Lektion gelernt haben.

Solche Eigenschaften werden optionale Eigenschaften genannt. Eine Schnittstelle kann optionale Eigenschaften enthalten, und wir verwenden die ?:Type-Annotation, um sie darzustellen, genau wie die optionalen Funktionsparameter.

(optional-properties.ts)

Im obigen Beispiel hat die Student-Schnittstelle die age-Eigenschaft, die optional ist. Wenn die age-Eigenschaft jedoch bereitgestellt wird, muss sie einen Wert des Typs number haben.

Im Fall des ross-Objekts, das ein Typ der Student-Schnittstelle ist, haben wir keinen Wert für die age-Eigenschaft bereitgestellt, was legal ist, im Fall von monica haben wir die age-Eigenschaft bereitgestellt, aber ihr Wert ist string, was nicht legal ist. Daher gibt der TypeScript-Compiler einen Fehler aus.

Der Fehler mag seltsam erscheinen, aber er macht tatsächlich Sinn. Wenn die Eigenschaft age für ein Objekt nicht existiert, gibt object.age undefined zurück, das ein Typ von undefined ist. Wenn sie existiert, muss der Wert vom Typ number sein.

Der age-Eigenschaftswert kann also entweder vom Typ undefined oder number sein, was in TypeScript durch die Union-Syntax number | undefined dargestellt wird.

💡 Wir werden Typ-Unions in einer Type-System-Lektion kennenlernen.

Doch optionale Eigenschaften werfen während der Programmausführung ernsthafte Probleme auf. Stellen wir uns vor, wir verwenden die Eigenschaft age in einer arithmetischen Operation, aber ihr Wert ist undefined. Das ist ein ernstes Problem.

Aber das Gute ist, dass der TypeScript-Compiler keine illegalen Operationen mit einer optionalen Eigenschaft zulässt, da ihr Wert undefined sein kann.

(optional-properties-safety.ts)

Im obigen Beispiel führen wir eine arithmetische Operation an der Eigenschaft age durch, was illegal ist, da der Wert dieser Eigenschaft zur Laufzeit number oder undefined sein kann. Die Durchführung von arithmetischen Operationen auf undefined ergibt NaN (keine Zahl).

💡 Für das obige Programm mussten wir jedoch das --strictNullChecks-Flag auf false setzen, das ein TypeScript-Compiler-Flag ist. Wenn wir diese Option bereitstellen, lässt sich das obige Programm problemlos kompilieren.

Um diesen Fehler oder diese Warnung zu vermeiden, müssen wir dem TypeScript-Compiler explizit mitteilen, dass diese Eigenschaft ein Typ von number und nicht von number oder undefined ist. Dazu verwenden wir die Type Assertion (auch bekannt als Typkonvertierung oder Typecasting).

(optional-properties-safety-override.ts)

Im obigen Programm haben wir (_student.age as number) verwendet, das den Typ von _student.age von number | undefined nach number konvertiert. Dies ist eine Möglichkeit, dem TypeScript-Compiler mitzuteilen: „Hey, das ist eine Zahl“. Ein besserer Weg wäre jedoch, zur Laufzeit zu prüfen, ob _student.age undefined ist, und dann die arithmetische Operation auszuführen.

💡 Über Type Assertions werden wir in einer Type System-Lektion lernen.

Funktionstyp mit einer Schnittstelle

Nicht nur die Form eines einfachen Objekts, sondern eine Schnittstelle kann auch die Signatur einer Funktion beschreiben. In der vorangegangenen Lektion haben wir Typ-Alias verwendet, um einen Funktionstyp zu beschreiben, aber Schnittstellen können das auch.

interface InterfaceName {
(param: Type): Type;
}

Die Syntax, um eine Schnittstelle als einen Funktionstyp zu deklarieren, ist ähnlich wie die Funktionssignatur selbst. Wie man im obigen Beispiel sehen kann, enthält der Körper der Schnittstelle die genaue Signatur einer anonymen Funktion, natürlich ohne den Körper. Hier spielen die Parameternamen keine Rolle.

(function-signature.ts)

Im obigen Beispiel haben wir eine IsSumOddSchnittstelle definiert, die einen Funktionstyp festlegt, der zwei Argumente vom Typ number annimmt und einen booleanWert zurückgibt. Jetzt können Sie diesen Typ verwenden, um eine Funktion zu beschreiben, weil der IsSumOdd-Schnittstellentyp dem Funktionstyp (x: number, y: number) => boolean entspricht.

Eine Schnittstelle mit einer anonymen Methodensignatur beschreibt eine Funktion. Aber eine Funktion in der JavaScript-Welt ist auch ein Objekt, was bedeutet, dass Sie einem Funktionswert Eigenschaften hinzufügen können wie einem Objekt. Daher ist es völlig legal, beliebige Eigenschaften an einer Schnittstelle des Funktionstyps zu definieren.

(function-signature-with-methods.ts)

Im obigen Beispiel haben wir type und calculate Eigenschaften an der Schnittstelle IsSumOdd hinzugefügt, die eine Funktion beschreibt. Mit der Methode Object.assign fügen wir type und calculate Eigenschaften mit einem Funktionswert zusammen.

Schnittstellen vom Typ Funktion können hilfreich sein, um Konstruktorfunktionen zu beschreiben. Eine Konstruktorfunktion ist vergleichbar mit einer Klasse, deren Aufgabe es ist, Objekte (Instanzen) zu erzeugen. Bis ES5 gab es nur Konstruktorfunktionen zur Nachahmung eines class in JavaScript. Daher kompiliert TypeScript Klassen zu Konstruktorfunktionen, wenn Sie auf ES5 oder darunter abzielen.

💡 Wenn Sie mehr über Konstruktorfunktionen erfahren möchten, lesen Sie diesen Artikel.

Wenn wir das Schlüsselwort new vor die Signatur einer anonymen Funktion in der Schnittstelle setzen, wird die Funktion konstruierbar. Das bedeutet, dass die Funktion nur mit dem Schlüsselwort new aufgerufen werden kann, um Objekte zu erzeugen, und nicht mit einem normalen Funktionsaufruf. Ein Beispiel für eine Konstruktorfunktion sieht wie folgt aus:

function Animal( _name ) {
this.name = _name;
}var dog = new Animal( 'Tommy' );
console.log( dog.name ); // Tommy

Glücklicherweise müssen wir nicht mit Konstruktorfunktionen arbeiten, da TypeScript das Schlüsselwort class zur Verfügung stellt, um eine Klasse zu erstellen, die viel einfacher zu handhaben ist als eine Konstruktorfunktion. Tatsächlich ist ein class in JavaScript eine Konstruktorfunktion. Probieren Sie das folgende Beispiel aus.

class Animal{
constructor( _name ) {
this.name = _name;
}
}console.log( typeof Animal ); // "function"

Eine Klasse und eine Konstruktorfunktion sind ein und dasselbe. Der einzige Unterschied besteht darin, dass die class uns eine reichhaltige OOP-Syntax zur Verfügung stellt, mit der wir arbeiten können. Daher stellt eine Schnittstelle eines Konstruktorfunktionstyps eine Klasse dar.

(funktion-signatur-mit-neu.ts)

Im obigen Beispiel haben wir die Klasse Animal mit einer Konstruktorfunktion definiert, die ein Argument des Typs string annimmt. Sie können dies als eine Konstruktorfunktion betrachten, die eine ähnliche Signatur wie der Animal-Konstruktor hat.

Die AnimalInterface definiert eine Konstruktorfunktion, da sie eine anonyme Funktion mit dem Schlüsselwort new vorangestellt hat. Das bedeutet, dass die Klasse Animal ein Typ von AnimalInterface sein kann. Hier ist der AnimalInterface-Schnittstellentyp äquivalent zum Funktionstyp new (sound: string) => any.

Die createAnimal-Funktion akzeptiert ctor-Argumente vom Typ AnimalInterface, daher können wir die Animal-Klasse als Argumentwert übergeben. Wir werden nicht in der Lage sein, getSound Methodensignatur der Animal Klasse in AnimalInterface hinzuzufügen und der Grund wird in der Lektion Klassen erklärt.

Indexierbare Typen

Ein indexierbares Objekt ist ein Objekt, auf dessen Eigenschaften mit einer Indexsignatur wie obj zugegriffen werden kann. Dies ist der Standardweg, um auf ein Array-Element zuzugreifen, aber wir können dies auch für das Objekt tun.

var a = ;
var o = { one: 1, two: 2, three: 3 };console.log( a ); // 1
console.log( a ); // 1 (same as `a.one`)

Es kann vorkommen, dass Ihr Objekt eine beliebige Anzahl von Eigenschaften hat, ohne eine bestimmte Form. In diesem Fall können Sie einfach den Typ object verwenden. Dieser object-Typ definiert jedoch jeden Wert, der nicht number, string, boolean, symbol, null oder undefined ist, wie in der Lektion über Grundtypen besprochen.

Wenn wir streng prüfen müssen, ob ein Wert ein einfaches JavaScript-Objekt ist, haben wir möglicherweise ein Problem. Dies kann mit einem Schnittstellentyp mit einer Indexsignatur für den Eigenschaftsnamen gelöst werden.

interface SimpleObject {
: any;
}

Die SimpleObject-Schnittstelle definiert die Form eines Objekts mit string-Schlüsseln, deren Werte vom any-Datentyp sein können. Hier wird der key Eigenschaftsname nur als Platzhalter verwendet, da er in eckige Klammern eingeschlossen ist.

(indexable-type-object.ts)

Im obigen Beispiel haben wir ross und monica Objekte vom Typ SimpleObject Schnittstelle definiert. Da diese Objekte string-Schlüssel und Werte vom any-Datentyp enthalten, ist das völlig legal.

Wenn Sie sich über den Schlüssel 1 in monica wundern, der vom Typ number ist, ist das legal, da Objekt- oder Array-Elemente in JavaScript mit number– oder string-Schlüsseln indiziert werden können, wie unten gezeigt.

var o = { 0: 'Zero', '1': 'One' };
var a = ;console.log( o ); // Zero
console.log( o ); // One
console.log( a ); // One
console.log( a ); // One

Wenn wir den Typ der Schlüssel und ihrer Werte genauer definieren müssen, können wir das natürlich auch tun. Zum Beispiel können wir einen indizierbaren Schnittstellentyp mit Schlüsseln vom Typ number und Werten vom Typ number definieren, wenn wir wollen.

💡 Der Schlüsseltyp einer Indexsignatur muss entweder string oder number sein.

(indexable-type-number.ts)

Im obigen Beispiel haben wir eine LapTimes Schnittstelle definiert, die Eigenschaftsnamen vom Typ number und Werte vom Typ number enthalten kann. Diese Schnittstelle kann eine Datenstruktur darstellen, die mit number-Schlüsseln indiziert werden kann, daher sind Array ross und Objekte monica und joey legal.

Das rachel-Objekt entspricht jedoch nicht der Form von LapTimes, da Schlüssel one ein string ist und nur mit string wie rachel und nichts anderem zugegriffen werden kann. Daher wird der TypeScript-Compiler einen Fehler wie oben gezeigt ausgeben.

Es ist möglich, einige Eigenschaften in einem indizierbaren Schnittstellentyp erforderlich und einige optional zu haben. Dies kann sehr nützlich sein, wenn ein Objekt eine bestimmte Form haben muss, aber es macht nichts, wenn wir zusätzliche und unerwünschte Eigenschaften im Objekt haben.

(indexable-type-required-properties.ts)

Im obigen Beispiel haben wir eine Schnittstelle LapTimes definiert, die die Eigenschaft name mit dem Wert string und die optionale Eigenschaft age mit dem Wert number enthalten muss. Ein Objekt vom Typ LapTimes kann auch beliebige Eigenschaften haben, deren Schlüssel number sein müssen und deren Werte ebenfalls number sein sollten.

Das Objekt ross ist ein gültiges LapTimes-Objekt, obwohl es die Eigenschaft age nicht hat, da sie optional ist. Das Objekt monica hat zwar die Eigenschaft age, aber sein Wert ist string und entspricht daher nicht der Schnittstelle LapTimes.

Das Objekt joey entspricht ebenfalls nicht der Schnittstelle LapTimes, da es eine Eigenschaft gender hat, die ein Typ von string ist. Das rachel-Objekt hat keine name-Eigenschaft, die in der LapTimes-Schnittstelle erforderlich ist.

💡 Bei der Verwendung indizierbarer Typen gibt es einige Probleme, auf die wir achten müssen. Diese werden in dieser Dokumentation erwähnt.

Erweiternde Schnittstelle

Wie Klassen kann eine Schnittstelle Eigenschaften von anderen Schnittstellen erben. Im Gegensatz zu Klassen in JavaScript kann eine Schnittstelle jedoch von mehreren Schnittstellen erben. Wir verwenden das Schlüsselwort extends, um eine Schnittstelle zu erben.

(extend-interface.ts)

Bei der Erweiterung einer Schnittstelle erhält die untergeordnete Schnittstelle alle Eigenschaften der übergeordneten Schnittstelle. Dies ist sehr nützlich, wenn mehrere Schnittstellen eine gemeinsame Struktur haben und wir Code-Duplizierung vermeiden wollen, indem wir die gemeinsamen Eigenschaften in eine gemeinsame Schnittstelle aufnehmen, die später vererbt werden kann.

(extend-multiple-interfaces.ts)

Im obigen Beispiel haben wir eine StudentSchnittstelle erstellt, die Eigenschaften von der Person und PlayerSchnittstelle erbt.

Mehrere Schnittstellendeklarationen

Im vorherigen Abschnitt haben wir gelernt, wie eine Schnittstelle die Eigenschaften einer anderen Schnittstelle erben kann. Dies geschah mit dem Schlüsselwort extend. Wenn jedoch Schnittstellen mit demselben Namen innerhalb desselben Moduls (Datei) deklariert werden, fasst TypeScript ihre Eigenschaften zusammen, solange sie unterschiedliche Eigenschaftsnamen haben oder ihre widersprüchlichen Eigenschaftstypen gleich sind.

(merged-interface.ts)

Im obigen Beispiel haben wir die Schnittstelle Person mehrmals deklariert. Das Ergebnis ist eine einzige Person-Schnittstellendeklaration, indem die Eigenschaften aller Person-Schnittstellendeklarationen zusammengeführt werden.

💡 In der Lektion Klassen haben wir gelernt, dass eine class implizit eine Schnittstelle deklariert und eine Schnittstelle diese Schnittstelle erweitern kann. Wenn also ein Programm eine Klasse Person und eine Schnittstelle Person hat, dann wird der endgültige Person Typ (Schnittstelle) verschmolzene Eigenschaften zwischen der Klasse und der Schnittstelle haben.

Schachtelschnittstellen

Eine Schnittstelle kann tief verschachtelte Strukturen haben. Im folgenden Beispiel definiert das infoFeld der StudentSchnittstelle die Form eines Objekts mit firstName und lastNameEigenschaften.

(nested-shape.ts)

Auch ist es völlig legal, dass ein Feld einer Schnittstelle den Typ einer anderen Schnittstelle hat. Im folgenden Beispiel hat das Feld info der Schnittstelle Student den Typ der Schnittstelle Person.

(nested-interface.ts)

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.