TypeScript meistern
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)


In diesem Kapitel über TypeScript untersuchen wir Typen, die sich auf Klassen und ihre Instanzen beziehen.

17.1 Die zwei Prototypketten von Klassen

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);
Figure 2: Objects created by class Counter. Left-hand side: the class and its superclass Object. Right-hand side: The instance myCounter, the prototype properties of Counter, and the prototype methods of the superclass Object..

Das Diagramm in Abb. 2 zeigt die Laufzeitstruktur der Klasse Counter. Es gibt zwei Prototypketten von Objekten in diesem Diagramm

In diesem Kapitel werden wir zuerst Instanzobjekte und dann Klassen als Objekte untersuchen.

17.2 Interfaces für Instanzen von Klassen

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

17.3 Interfaces für Klassen

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.

17.3.1 Beispiel: Konvertierung von und nach JSON

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

17.3.2 Beispiel: TypeScript's eingebaute Interfaces für die Klasse Object und deren Instanzen

Es 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

17.4 Klassen als Typen

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];
}

17.4.1 Fallstrick: Klassen funktionieren strukturell, nicht nominal

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.

17.4.1.1 Strukturelles Typing ausschalten

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.

17.5 Weiterführende Lektüre