EMZETT.
Login

Kurz: TypeScript ergänzt JavaScript-Klassen um Typen, Sichtbarkeit und abstrakte Klassen.

Teil des Kurses TypeScript

Kapitel 7 von 15 im Kurs TypeScript (Abschnitt „Fortgeschritten“). Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

TypeScript ergänzt JavaScript-Klassen um Typen, Sichtbarkeit und abstrakte Klassen.

Eigenschaften und Methoden

class Konto {
  readonly inhaber: string;
  #stand: number = 0;              // echt privat (JavaScript)
  private log: string[] = [];      // nur zur Compile-Zeit privat (TypeScript)
 
  constructor(inhaber: string, startwert = 0) {
    this.inhaber = inhaber;
    this.#stand = startwert;
  }
 
  einzahlen(betrag: number): this {
    if (betrag <= 0) throw new RangeError("Betrag muss positiv sein");
    this.#stand += betrag;
    this.log.push(`+${betrag}`);
    return this;
  }
 
  get stand(): number {
    return this.#stand;
  }
 
  verlauf(): string[] {
    return [...this.log];
  }
}
 
const k = new Konto("Mia", 100).einzahlen(50).einzahlen(25);
console.log(k.inhaber, k.stand, k.verlauf());

Ausgabe:

Mia 175 [ '+50', '+25' ]
ModifikatorWirkung
publicüberall sichtbar (Standard)
privatenur in der Klasse (Compile-Zeit)
protectedin der Klasse und Unterklassen
readonlynur im Konstruktor zuweisbar
#nameecht privat zur Laufzeit
staticgehört zur Klasse
class A { private geheim = 1; }
const a = new A();
console.log(a.geheim);

Fehlermeldung:

error TS2341: Property 'geheim' is private and only accessible within class 'A'.

Interfaces umsetzen

Eine Klasse kann einen Vertrag erfüllen müssen:

interface Form {
  flaeche(): number;
  readonly name: string;
}
 
class Kreis implements Form {
  readonly name = "Kreis";
  constructor(private radius: number) {}
  flaeche(): number { return Math.PI * this.radius ** 2; }
}

Hinweis

Die Kurzschreibweise constructor(private radius: number) (Parameter-Eigenschaften) ist praktisch, wird aber von manchen Werkzeugen, die Typen nur entfernen, nicht unterstützt. Ausführlich schreiben ist die portable Variante.

interface Form { flaeche(): number; readonly name: string }
 
class Rechteck implements Form {
  readonly name = "Rechteck";
  breite: number;
  hoehe: number;
  constructor(breite: number, hoehe: number) { this.breite = breite; this.hoehe = hoehe; }
  flaeche(): number { return this.breite * this.hoehe; }
}
class Quadrat extends Rechteck {
  constructor(seite: number) { super(seite, seite); }
}
const formen: Form[] = [new Rechteck(2, 3), new Quadrat(4)];
console.log(formen.map((f) => f.flaeche()));

Ausgabe:

[ 6, 16 ]

Abstrakte Klassen

Eine abstrakte Klasse ist nur Vorlage und kann nicht direkt instanziiert werden:

abstract class Tier {
  constructor(protected name: string) { }
  abstract laut(): string;                    // Unterklassen müssen das umsetzen
  vorstellen(): string { return `${this.name} macht ${this.laut()}`; }
}

In reinem JavaScript-Stil, damit er auch ohne Compiler läuft:

abstract class Tier {
  name: string;
  constructor(name: string) { this.name = name; }
  abstract laut(): string;
  vorstellen(): string { return `${this.name} macht ${this.laut()}`; }
}
class Hund extends Tier { laut() { return "Wuff"; } }
class Katze extends Tier { laut() { return "Miau"; } }
for (const t of [new Hund("Rex"), new Katze("Mimi")]) console.log(t.vorstellen());

Ausgabe:

Rex macht Wuff
Mimi macht Miau
abstract class Tier { abstract laut(): string }
const t = new Tier();

Fehlermeldung:

error TS2511: Cannot create an instance of an abstract class.

Statische Member und Getter/Setter

class Temperatur {
  static readonly ABSOLUTER_NULLPUNKT = -273.15;
  #celsius = 0;
  get celsius(): number { return this.#celsius; }
  set celsius(w: number) {
    if (w < Temperatur.ABSOLUTER_NULLPUNKT) throw new RangeError("zu kalt");
    this.#celsius = w;
  }
  get fahrenheit(): number { return this.#celsius * 9 / 5 + 32; }
  static ausFahrenheit(f: number): Temperatur {
    const t = new Temperatur();
    t.celsius = (f - 32) * 5 / 9;
    return t;
  }
}
const t = Temperatur.ausFahrenheit(212);
console.log(t.celsius, t.fahrenheit);

Ausgabe:

100 212

Klassen als Typ

Der Name einer Klasse ist zugleich ein Typ für ihre Instanzen:

class Punkt { constructor(public x = 0, public y = 0) {} }

Auch hier gilt: strukturelle Typisierung. Ein Objekt { x: 1, y: 2 } passt zum Typ Punkt, obwohl es keine Instanz ist.

Composition over Inheritance

Vererbung koppelt Klassen eng. Oft besser ist Komposition: Eine Klasse bekommt andere Objekte über das Interface und nutzt sie.

interface Speicher { speichere(k: string, v: string): void; lade(k: string): string | undefined }
 
class MemSpeicher implements Speicher {
  private daten = new Map<string, string>();
  speichere(k: string, v: string) { this.daten.set(k, v); }
  lade(k: string) { return this.daten.get(k); }
}
class Einstellungen {
  speicher: Speicher;
  constructor(speicher: Speicher) { this.speicher = speicher; }     // Abhängigkeit hereingegeben
  setzeThema(t: "hell" | "dunkel") { this.speicher.speichere("thema", t); }
  thema() { return this.speicher.lade("thema") ?? "hell"; }
}
const e = new Einstellungen(new MemSpeicher());
console.log(e.thema());
e.setzeThema("dunkel");
console.log(e.thema());

Ausgabe:

hell
dunkel

Im Test kannst du jederzeit einen anderen Speicher hineingeben, im Browser etwa localStorage.

Merke

  • Sichtbarkeit: public, private, protected, readonly; #name ist echt privat
  • implements erzwingt, dass eine Klasse ein Interface erfüllt
  • abstract für Vorlagen; static für Klassenebene
  • Klassen sind strukturell typisiert
  • Komposition und Schnittstellen halten Code flexibel und testbar

Übungsaufgabe

Baue eine abstrakte Klasse Mitarbeiter mit gehalt() und zwei Unterklassen (Angestellter, Freelancer) und summiere die Gehälter einer gemischten Liste.

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Arrays, Tupel und Enums

Weiter: Generics

Alle Kapitel: TypeScript im Überblick