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.
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.
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.
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.
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
.
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.
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.
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.
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 auffalse
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).
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.
Im obigen Beispiel haben wir eine IsSumOdd
Schnittstelle definiert, die einen Funktionstyp festlegt, der zwei Argumente vom Typ number
annimmt und einen boolean
Wert 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.
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.
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.
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
odernumber
sein.
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.
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.
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.
Im obigen Beispiel haben wir eine Student
Schnittstelle erstellt, die Eigenschaften von der Person
und Player
Schnittstelle 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.
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 KlassePerson
und eine SchnittstellePerson
hat, dann wird der endgültigePerson
Typ (Schnittstelle) verschmolzene Eigenschaften zwischen der Klasse und der Schnittstelle haben.
Schachtelschnittstellen
Eine Schnittstelle kann tief verschachtelte Strukturen haben. Im folgenden Beispiel definiert das info
Feld der Student
Schnittstelle die Form eines Objekts mit firstName
und lastName
Eigenschaften.
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
.