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

16 Klassendefinitionen in TypeScript



In diesem Kapitel untersuchen wir, wie Klassendefinitionen in TypeScript funktionieren.

16.1 Spickzettel: Klassen in reinem JavaScript

Dieser Abschnitt ist ein Spickzettel für Klassendefinitionen in reinem JavaScript.

16.1.1 Grundlegende Klassenmember

class OtherClass {}

class MyClass1 extends OtherClass {

  publicInstanceField = 1;

  constructor() {
    super();
  }

  publicPrototypeMethod() {
    return 2;
  }
}

const inst1 = new MyClass1();
assert.equal(inst1.publicInstanceField, 1);
assert.equal(inst1.publicPrototypeMethod(), 2);

  Die folgenden Abschnitte handeln von Modifikatoren

Am Ende gibt es eine Tabelle, die zeigt, wie Modifikatoren kombiniert werden können.

16.1.2 Modifikator: static

class MyClass2 {

  static staticPublicField = 1;

  static staticPublicMethod() {
    return 2;
  }
}

assert.equal(MyClass2.staticPublicField, 1);
assert.equal(MyClass2.staticPublicMethod(), 2);

16.1.3 Präfix für Modifikatoren: # (private)

class MyClass3 {
  #privateField = 1;

  #privateMethod() {
    return 2;
  }

  static accessPrivateMembers() {
    // Private members can only be accessed from inside class definitions
    const inst3 = new MyClass3();
    assert.equal(inst3.#privateField, 1);
    assert.equal(inst3.#privateMethod(), 2);
  }
}
MyClass3.accessPrivateMembers();

Warnung für JavaScript

TypeScript unterstützt private Felder seit Version 3.8, aber derzeit keine privaten Methoden.

16.1.4 Modifikatoren für Accessoren: get (Getter) und set (Setter)

Accessoren sind im Grunde Methoden, die durch den Zugriff auf Eigenschaften aufgerufen werden. Es gibt zwei Arten von Accessoren: Getter und Setter.

class MyClass5 {
  #name = 'Rumpelstiltskin';
  
  /** Prototype getter */
  get name() {
    return this.#name;
  }

  /** Prototype setter */
  set name(value) {
    this.#name = value;
  }
}
const inst5 = new MyClass5();
assert.equal(inst5.name, 'Rumpelstiltskin'); // getter
inst5.name = 'Queen'; // setter
assert.equal(inst5.name, 'Queen'); // getter

16.1.5 Modifikator für Methoden: * (Generator)

class MyClass6 {
  * publicPrototypeGeneratorMethod() {
    yield 'hello';
    yield 'world';
  }
}

const inst6 = new MyClass6();
assert.deepEqual(
  [...inst6.publicPrototypeGeneratorMethod()],
  ['hello', 'world']);

16.1.6 Modifikator für Methoden: async

class MyClass7 {
  async publicPrototypeAsyncMethod() {
    const result = await Promise.resolve('abc');
    return result + result;
  }
}

const inst7 = new MyClass7();
inst7.publicPrototypeAsyncMethod()
  .then(result => assert.equal(result, 'abcabc'));

16.1.7 Berechenbare Klassennamen

const publicInstanceFieldKey = Symbol('publicInstanceFieldKey');
const publicPrototypeMethodKey = Symbol('publicPrototypeMethodKey');

class MyClass8 {

  [publicInstanceFieldKey] = 1;

  [publicPrototypeMethodKey]() {
    return 2;
  }
}

const inst8 = new MyClass8();
assert.equal(inst8[publicInstanceFieldKey], 1);
assert.equal(inst8[publicPrototypeMethodKey](), 2);

Kommentare

16.1.8 Kombinationen von Modifikatoren

Felder (kein Level bedeutet, dass ein Konstrukt auf Instanzebene existiert)

Level Sichtbarkeit
(Instanz)
(Instanz) #
Statisch
Statisch #

Methoden (kein Level bedeutet, dass ein Konstrukt auf Prototyp-Ebene existiert)

Level Accessor Async Generator Sichtbarkeit
(Prototyp)
(Prototyp) get
(Prototyp) set
(Prototyp) async
(Prototyp) *
(Prototyp) async *
(Prototyp-bezogen) #
(Prototyp-bezogen) get #
(Prototyp-bezogen) set #
(Prototyp-bezogen) async #
(Prototyp-bezogen) * #
(Prototyp-bezogen) async * #
Statisch
Statisch get
Statisch set
Statisch async
Statisch *
Statisch async *
Statisch #
Statisch get #
Statisch set #
Statisch async #
Statisch * #
Statisch async * #

Einschränkungen von Methoden

16.1.9 Hinter den Kulissen

Es ist wichtig zu bedenken, dass bei Klassen zwei Ketten von Prototypobjekten existieren.

Betrachten wir das folgende reine JavaScript-Beispiel:

class ClassA {
  static staticMthdA() {}
  constructor(instPropA) {
    this.instPropA = instPropA;
  }
  prototypeMthdA() {}
}
class ClassB extends ClassA {
  static staticMthdB() {}
  constructor(instPropA, instPropB) {
    super(instPropA);
    this.instPropB = instPropB;
  }
  prototypeMthdB() {}
}
const instB = new ClassB(0, 1);

Abb. 1 1 zeigt, wie die Prototypketten aussehen, die von ClassA und ClassB erstellt werden.

Figure 1: The classes ClassA and ClassB create two prototype chains: One for classes (left-hand side) and one for instances (right-hand side).

16.1.10 Mehr Informationen zu Klassendefinitionen in reinem JavaScript

16.2 Nicht öffentliche Datenslots in TypeScript

Standardmäßig sind alle Datenslots in TypeScript öffentliche Eigenschaften. Es gibt zwei Möglichkeiten, Daten privat zu halten:

Wir betrachten beide als Nächstes.

Beachten Sie, dass TypeScript derzeit keine privaten Methoden unterstützt.

16.2.1 Private Eigenschaften

Private Eigenschaften sind ein (statisches) Feature nur in TypeScript. Jede Eigenschaft kann privat gemacht werden, indem ihr das Schlüsselwort private vorangestellt wird (Zeile A).

class PersonPrivateProperty {
  private name: string; // (A)
  constructor(name: string) {
    this.name = name;
  }
  sayHello() {
    return `Hello ${this.name}!`;
  }
}

Wir erhalten nun Kompilierungsfehler, wenn wir auf diese Eigenschaft im falschen Geltungsbereich zugreifen (Zeile A).

const john = new PersonPrivateProperty('John');

assert.equal(
  john.sayHello(), 'Hello John!');

// @ts-expect-error: Property 'name' is private and only accessible
// within class 'PersonPrivateProperty'. (2341)
john.name; // (A)

private ändert jedoch nichts zur Laufzeit. Dort ist die Eigenschaft .name von einer öffentlichen Eigenschaft nicht zu unterscheiden.

assert.deepEqual(
  Object.keys(john),
  ['name']);

Wir können auch sehen, dass private Eigenschaften zur Laufzeit nicht geschützt sind, wenn wir uns den JavaScript-Code ansehen, zu dem die Klasse kompiliert wird.

class PersonPrivateProperty {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    return `Hello ${this.name}!`;
  }
}

16.2.2 Private Felder

Private Felder sind ein neues JavaScript-Feature, das TypeScript seit Version 3.8 unterstützt.

class PersonPrivateField {
  #name: string;
  constructor(name: string) {
    this.#name = name;
  }
  sayHello() {
    return `Hello ${this.#name}!`;
  }
}

Diese Version von Person wird weitgehend genauso verwendet wie die Version mit privaten Eigenschaften.

const john = new PersonPrivateField('John');

assert.equal(
  john.sayHello(), 'Hello John!');

Dieses Mal ist die Eingekapselung jedoch vollständig. Die Verwendung der privaten Feld-Syntax außerhalb von Klassen ist sogar ein JavaScript-Syntaxfehler. Deshalb müssen wir eval() in Zeile A verwenden, damit wir diesen Code ausführen können.

assert.throws(
  () => eval('john.#name'), // (A)
  {
    name: 'SyntaxError',
    message: "Private field '#name' must be declared in "
      + "an enclosing class",
  });

assert.deepEqual(
  Object.keys(john),
  []);

Das Kompilierungsergebnis ist nun viel komplizierter (leicht vereinfacht).

var __classPrivateFieldSet = function (receiver, privateMap, value) {
  if (!privateMap.has(receiver)) {
    throw new TypeError(
      'attempted to set private field on non-instance');
  }
  privateMap.set(receiver, value);
  return value;
};

// Omitted: __classPrivateFieldGet

var _name = new WeakMap();
class Person {
  constructor(name) {
    // Add an entry for this instance to _name
    _name.set(this, void 0);

    // Now we can use the helper function:
    __classPrivateFieldSet(this, _name, name);
  }
  // ···
}

Dieser Code verwendet eine gängige Technik, um Instanzdaten privat zu halten.

Weitere Informationen zu diesem Thema finden Sie unter „JavaScript for impatient programmers“.

16.2.3 Private Eigenschaften vs. private Felder

16.2.4 Protected Eigenschaften

Private Felder und private Eigenschaften können nicht in Unterklassen aufgerufen werden (Zeile A).

class PrivatePerson {
  private name: string;
  constructor(name: string) {
    this.name = name;
  }
  sayHello() {
    return `Hello ${this.name}!`;
  }
}
class PrivateEmployee extends PrivatePerson {
  private company: string;
  constructor(name: string, company: string) {
    super(name);
    this.company = company;
  }
  sayHello() {
    // @ts-expect-error: Property 'name' is private and only
    // accessible within class 'PrivatePerson'. (2341)
    return `Hello ${this.name} from ${this.company}!`; // (A)
  }  
}

Wir können das vorherige Beispiel korrigieren, indem wir in Zeile A von private auf protected umstellen (wir stellen auch in Zeile B um, der Konsistenz halber).

class ProtectedPerson {
  protected name: string; // (A)
  constructor(name: string) {
    this.name = name;
  }
  sayHello() {
    return `Hello ${this.name}!`;
  }
}
class ProtectedEmployee extends ProtectedPerson {
  protected company: string; // (B)
  constructor(name: string, company: string) {
    super(name);
    this.company = company;
  }
  sayHello() {
    return `Hello ${this.name} from ${this.company}!`; // OK
  }  
}

16.3 Private Konstruktoren

Konstruktoren können ebenfalls privat sein. Das ist nützlich, wenn wir statische Factory-Methoden haben und möchten, dass Kunden immer diese Methoden verwenden und niemals den Konstruktor direkt aufrufen. Statische Methoden können private Klassenmember aufrufen, weshalb die Factory-Methoden den Konstruktor weiterhin verwenden können.

Im folgenden Code gibt es eine statische Factory-Methode DataContainer.create(). Sie richtet Instanzen über asynchron geladene Daten ein. Die asynchronen Codezeilen in der Factory-Methode zu halten, ermöglicht es der eigentlichen Klasse, vollständig synchron zu sein.

class DataContainer {
  #data: string;
  static async create() {
    const data = await Promise.resolve('downloaded'); // (A)
    return new this(data);
  }
  private constructor(data: string) {
    this.#data = data;
  }
  getData() {
    return 'DATA: '+this.#data;
  }
}
DataContainer.create()
  .then(dc => assert.equal(
    dc.getData(), 'DATA: downloaded'));

In realem Code würden wir fetch() oder eine ähnliche Promise-basierte API verwenden, um Daten asynchron in Zeile A zu laden.

Der private Konstruktor verhindert, dass DataContainer unterklassenfähig ist. Wenn wir Unterklassen zulassen wollen, müssen wir ihn auf protected setzen.

16.4 Initialisierung von Instanzeigenschaften

16.4.1 Strikte Eigenschaftsinitialisierung

Wenn die Compiler-Einstellung --strictPropertyInitialization aktiviert ist (was der Fall ist, wenn wir --strict verwenden), prüft TypeScript, ob alle deklarierten Instanzeigenschaften korrekt initialisiert werden.

Manchmal initialisieren wir Eigenschaften jedoch auf eine Weise, die TypeScript nicht erkennt. Dann können wir Ausrufezeichen (definite assignment assertions) verwenden, um die Warnungen von TypeScript abzuschalten (Zeile A und Zeile B).

class Point {
  x!: number; // (A)
  y!: number; // (B)
  constructor() {
    this.initProperties();
  }
  initProperties() {
    this.x = 0;
    this.y = 0;
  }
}
16.4.1.1 Beispiel: Einrichten von Instanzeigenschaften über Objekte

Im folgenden Beispiel benötigen wir ebenfalls „definite assignment assertions“. Hier richten wir Instanzeigenschaften über den Konstruktorparameter props ein.

class CompilerError implements CompilerErrorProps { // (A)
  line!: number;
  description!: string;
  constructor(props: CompilerErrorProps) {
    Object.assign(this, props); // (B)
  }
}

// Helper interface for the parameter properties
interface CompilerErrorProps {
  line: number,
  description: string,
}

// Using the class:
const err = new CompilerError({
  line: 123,
  description: 'Unexpected token',
});

Hinweise

16.4.2 Konstruktorparameter public, private oder protected machen

Wenn wir das Schlüsselwort public für einen Konstruktorparameter verwenden, dann tut TypeScript zwei Dinge für uns:

Daher sind die folgenden beiden Klassen äquivalent:

class Point1 {
  constructor(public x: number, public y: number) {
  }
}

class Point2 {
  x: number;
  y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

Wenn wir anstelle von public private oder protected verwenden, sind die entsprechenden Instanzeigenschaften privat oder geschützt (nicht öffentlich).

16.5 Abstrakte Klassen

Zwei Konstrukte können in TypeScript abstrakt sein:

Der folgende Code demonstriert abstrakte Klassen und Methoden.

Einerseits gibt es die abstrakte Oberklasse Printable und ihre Hilfsklasse StringBuilder.

class StringBuilder {
  string = '';
  add(str: string) {
    this.string += str;
  }
}
abstract class Printable {
  toString() {
    const out = new StringBuilder();
    this.print(out);
    return out.string;
  }
  abstract print(out: StringBuilder): void;
}

Andererseits gibt es die konkreten Unterklassen Entries und Entry.

class Entries extends Printable {
  entries: Entry[];
  constructor(entries: Entry[]) {
    super();
    this.entries = entries;
  }
  print(out: StringBuilder): void {
    for (const entry of this.entries) {
      entry.print(out);
    }
  }
}
class Entry extends Printable {
  key: string;
  value: string;
  constructor(key: string, value: string) {
    super();
    this.key = key;
    this.value = value;
  }
  print(out: StringBuilder): void {
    out.add(this.key);
    out.add(': ');
    out.add(this.value);
    out.add('\n');
  }
}

Und schließlich verwenden wir Entries und Entry.

const entries = new Entries([
  new Entry('accept-ranges', 'bytes'),
  new Entry('content-length', '6518'),
]);
assert.equal(
  entries.toString(),
  'accept-ranges: bytes\ncontent-length: 6518\n');

Hinweise zu abstrakten Klassen