In diesem Kapitel über TypeScript untersuchen wir Typen, die sich auf Klassen und ihre Instanzen beziehen.
Betrachten wir diese Klasse
class Counter extends Object {
static createZero() {
return new Counter(0);
}
value: number;
constructor(value: number) {
super();
this.value = value;
}
increment() {
this.value++;
}
}
// Static method
const myCounter = Counter.createZero();
assert.ok(myCounter instanceof Counter);
assert.equal(myCounter.value, 0);
// Instance method
myCounter.increment();
assert.equal(myCounter.value, 1);Das Diagramm in Abb. 2 zeigt die Laufzeitstruktur der Klasse Counter. Es gibt zwei Prototypketten von Objekten in diesem Diagramm
Counter aufgebaut ist. Das Prototypobjekt der Klasse Counter ist ihre Oberklasse, Object.myCounter aufgebaut ist. Die Kette beginnt mit der Instanz myCounter und geht weiter mit Counter.prototype (das die Prototyp-Methoden der Klasse Counter enthält) und Object.prototype (das die Prototyp-Methoden der Klasse Object enthält).In diesem Kapitel werden wir zuerst Instanzobjekte und dann Klassen als Objekte untersuchen.
Interfaces spezifizieren Dienste, die Objekte anbieten. Zum Beispiel
interface CountingService {
value: number;
increment(): void;
}TypeScript's Interfaces funktionieren strukturell: Damit ein Objekt ein Interface implementiert, muss es lediglich die richtigen Eigenschaften mit den richtigen Typen haben. Das sehen wir im folgenden Beispiel
const myCounter2: CountingService = new Counter(3);Strukturelle Interfaces sind praktisch, da wir Interfaces auch für bereits existierende Objekte erstellen können (d.h. wir können sie nachträglich einführen).
Wenn wir im Voraus wissen, dass ein Objekt ein bestimmtes Interface implementieren muss, ist es oft sinnvoll, dies frühzeitig zu überprüfen, um spätere Überraschungen zu vermeiden. Das können wir für Instanzen von Klassen über implements tun
class Counter implements CountingService {
// ···
};Kommentare
.increment) und eigenen Eigenschaften (wie .value).Klassen selbst sind ebenfalls Objekte (Funktionen). Daher können wir Interfaces verwenden, um ihre Eigenschaften zu spezifizieren. Der Hauptanwendungsfall ist hier die Beschreibung von Fabriken für Objekte. Der nächste Abschnitt gibt ein Beispiel.
Die folgenden beiden Interfaces können für Klassen verwendet werden, die die Konvertierung ihrer Instanzen von und nach JSON unterstützen
// Converting JSON to instances
interface JsonStatic {
fromJson(json: any): JsonInstance;
}
// Converting instances to JSON
interface JsonInstance {
toJson(): any;
}Wir verwenden diese Interfaces im folgenden Code
class Person implements JsonInstance {
static fromJson(json: any): Person {
if (typeof json !== 'string') {
throw new TypeError(json);
}
return new Person(json);
}
name: string;
constructor(name: string) {
this.name = name;
}
toJson(): any {
return this.name;
}
}So können wir sofort überprüfen, ob die Klasse Person (als Objekt) das Interface JsonStatic implementiert
// Assign the class to a type-annotated variable
const personImplementsJsonStatic: JsonStatic = Person;Die folgende Art, diese Überprüfung durchzuführen, mag wie eine gute Idee erscheinen
const Person: JsonStatic = class implements JsonInstance {
// ···
};Das funktioniert jedoch nicht wirklich
Person nicht mit new aufrufen, da JsonStatic keine Konstruktor-Signatur hat.Person statische Eigenschaften über .fromJson() hinaus hat, lässt TypeScript uns diese nicht zugreifen.Object und deren InstanzenEs ist lehrreich, sich die eingebauten Typen von TypeScript anzusehen
Einerseits ist das Interface ObjectConstructor für die Klasse Object selbst
/**
* Provides functionality common to all JavaScript objects.
*/
declare var Object: ObjectConstructor;
interface ObjectConstructor {
new(value?: any): Object;
(): any;
(value: any): any;
/** A reference to the prototype for a class of objects. */
readonly prototype: Object;
/**
* Returns the prototype of an object.
* @param o The object that references the prototype.
*/
getPrototypeOf(o: any): any;
}Andererseits ist das Interface Object für Instanzen von Object
interface Object {
/** The initial value of Object.prototype.constructor is the standard built-in Object constructor. */
constructor: Function;
/** Returns a string representation of an object. */
toString(): string;
}Der Name Object wird zweimal verwendet, auf zwei verschiedenen Sprachebenen
Betrachten wir die folgende Klasse
class Color {
name: string;
constructor(name: string) {
this.name = name;
}
}Diese Klassendefinition erzeugt zwei Dinge.
Erstens eine Konstruktorfunktion namens Color (die über new aufgerufen werden kann)
assert.equal(
typeof Color, 'function')Zweitens ein Interface namens Color, das zu Instanzen von Color passt
const green: Color = new Color('green');Hier ist der Beweis, dass Color wirklich ein Interface ist
interface RgbColor extends Color {
rgbValue: [number, number, number];
}Es gibt jedoch einen Fallstrick: Die Verwendung von Color als statischer Typ ist keine sehr strenge Überprüfung
class Color {
name: string;
constructor(name: string) {
this.name = name;
}
}
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
const person: Person = new Person('Jane');
const color: Color = person; // (A)Warum gibt TypeScript in Zeile A keine Fehlermeldung aus? Das liegt am strukturellen Typing: Instanzen von Person und von Color haben die gleiche Struktur und sind daher statisch kompatibel.
Wir können die beiden Objektgruppen inkompatibel machen, indem wir private Eigenschaften hinzufügen
class Color {
name: string;
private branded = true;
constructor(name: string) {
this.name = name;
}
}
class Person {
name: string;
private branded = true;
constructor(name: string) {
this.name = name;
}
}
const person: Person = new Person('Jane');
// @ts-expect-error: Type 'Person' is not assignable to type 'Color'.
// Types have separate declarations of a private property
// 'branded'. (2322)
const color: Color = person;Die privaten Eigenschaften schalten in diesem Fall das strukturelle Typing aus.