Eigenschaften & Methoden

Klassen bestehen aus Eigenschaften (Daten) und Methoden (Operationen). Mithilfe von Zugriffsmodifizierern kann Kapselung erreicht werden: der Zugriff auf die Eigenschaften wird eingeschränkt und über Methoden kontrolliert. Diese Einschränkungen sind entscheidend um Abstraktionen entwickeln zu können.

Eigenschaften (Properties)

Eine Eigenschaft wird mit einem Namen und einem Datentyp deklariert. Jede Eigenschaft sollte initialisiert werden. Optionale Eigenschaften werden mit dem Suffix ? deklariert und müssen nicht initialisiert werden. Dank Typinferenz muss der Datentyp nicht unbedingt angegeben werden, wenn ein Initialwert definiert ist. Für eine bessere Lesbarkeit empfiehlt es sich aber, jede Eigenschaft mit einer Typannotation zu versehen. Hierzu ein Beispiel:

class Person {
  firstName: string = "";
  middleName?: string;
  lastName: string = "";
  born: Date = new Date(2000, 0, 1);
  died?: Date;
}
Info

Der Konstruktor der Klasse Date erwartet den Monat mit einem 0-basierenden Index (0: Januar, 1: Februar, …, 11: Dezember). new Date(2025, 11, 30) bezeichnet also den 30. Dezember 2025, nicht den 30. November 2025.

Die Klasse Person verfügt über einen Vornamen (firstName), einen optionalen Zweitnamen (middleName) und über einen Nachnamen (lastName). Weiter verfügt die Klasse über ein Geburts- (born) sowie ein optionales Todesdatum (died). Die Klasse kann folgendermassen verwendet werden:

const joe: Person = new Person();
joe.firstName = "Joe";
joe.lastName = "Doe";
joe.born = new Date(1968, 4, 9);

console.log(`${joe.firstName} ${joe.lastName} (*${joe.born})`);

Würde man Person als reinen Objekttyp definieren, sähe der Code folgendermassen aus:

type Person = {
  firstName: string;
  middleName?: string;
  lastName: string;
  born: Date;
  died?: Date;
};

const joe: Person = {
  firstName: "Joe",
  lastName: "Doe",
  born: new Date(1968, 4, 9),
};

console.log(`${joe.firstName} ${joe.lastName} (*${joe.born})`);

Dieser Code sieht wesentlich kompakter aus. Als reine Datenbehälter sind Objekttypen also offenbar besser geeignet als Klassen.

Methoden (Methods)

Klassen sind erst dann gegenüber Objekttypen von Vorteil, wenn Methoden auf den die Daten eines Objekts zugreifen sollen. Eine Methode ist eine Funktion, welche auf der Instanz einer Klasse operiert und mit dem this-Schlüsselwort Zugriff auf das konkrete Objekt (die Instanz) erhält:

class Person {
  firstName: string = "";
  middleName?: string;
  lastName: string = "";
  born: Date = new Date(2000, 0, 1);
  died?: Date;

  show(): string {
    let fullName: string;
    if (this.middleName) {
      fullName = `${this.firstName} ${this.middleName} ${this.lastName}`;
    } else {
      fullName = `${this.firstName} ${this.lastName}`;
    }
    let lifeSpan: string = `* ${this.formatYMD(this.born)}`;
    if (this.died) {
      lifeSpan += `, \u{1f547} ${this.formatYMD(this.died!)}`;
    }
    return `${fullName} (${lifeSpan})`;
  }

  formatYMD(d: Date) {
    return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
  }
}

Die Methode show gibt einen String zurück, der den vollen Namen und die Lebensdaten der jeweiligen Person enthält. Die Hilfsmethode formatYMD formatiert ein gegebenes Datum nach dem Format YYYY-MM-DD.

Diese Klasse kann nun folgendermassen verwendet werden:

const joe: Person = new Person();
joe.firstName = "Joe";
joe.middleName = "M.";
joe.lastName = "Doe";
joe.born = new Date(1968, 12, 23);
joe.died = new Date(2026, 11, 15);

const jane: Person = new Person();
jane.firstName = "Jane";
jane.lastName = "Done";
jane.born = new Date(1973, 10, 30);

console.log(joe.show());
console.log(jane.show());

Dadurch wurde eine gewisse Kapselung erreicht: Die show()-Methode kümmert sich um die Details der Formatierung. Je nachdem, welche Daten definiert werden (optionaler Zweitname und optionales Todesdatum), sieht das Ergebnis dann anders aus:

Joe M. Doe (* 1969-0-23, 🕇 2026-11-15)
Jane Done (* 1973-10-30)

Konstruktoren (Constructors)

Objektliteralen haben den Vorteil, dass man die initialen Daten bei der Objekterstellung mitgeben kann, ohne mehrere Zuweisungesoperationen durchführen zu müssen.

Mithilfe von einem Konstruktor können die initialen Daten dem Objekt bei seiner Erstellung mitgegeben werden:

class Person {
  firstName: string = "";
  middleName?: string;
  lastName: string = "";
  born: Date = new Date(2000, 0, 1);
  died?: Date;

  constructor(
    firstName: string,
    lastName: string,
    born: Date,
    middleName?: string,
    died?: Date,
  ) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.born = born;
    this.middleName = middleName;
    this.died = died;
  }

  // ...
}

Eigenschaften, die nicht zwingend benötigt werden, können über optionale Konstruktorparameter mitgegeben werden. Optionale Parameter sind mit dem Suffix ? bezeichnet stehen am Ende der Parameterliste.

Instanzen der Klasse Person lassen sich nun folgendermassen initialisieren:

const joe: Person = new Person(
  "Joe",
  "Doe",
  new Date(1968, 12, 23),
  "M.",
  new Date(2026, 11, 15),
);
const jane: Person = new Person("Jane", "Done", new Date(1973, 10, 30));

console.log(joe.show());
console.log(jane.show());

Eine bereits initialisierte Eigenschaft wird durch die Zuweisung im Konstruktor überschrieben.

Unveränderbarkeit (Immutability)

Es gibt Eigenschaften, die sich nach der Initialisierung nicht mehr ändern können. Solche unveränderbaren Eigenschaftgen werden mit dem Schlüsselwort readonly bezeichnet. Eine readonly-Eigenschaft muss entweder direkt oder spätestens im Konstruktor initialisiert werden:

class Person {
  firstName: string = "";
  middleName?: string;
  lastName: string = "";
  readonly born: Date;
  died?: Date;

  constructor(
    firstName: string,
    lastName: string,
    born: Date,
    middleName?: string,
    died?: Date,
  ) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.born = born;
    this.middleName = middleName;
    this.died = died;
  }
  // ...
}

Die Eigenschaft born wurde als readonly deklariert. Sie wird nicht mehr direkt initialisiert, sondern erhält im Konstruktor einen Wert. Die Eigenschaft kann nun nicht mehr überschrieben werden:

const joe: Person = new Person(
  "Joe",
  "Doe",
  new Date(1968, 12, 23),
  "M.",
  new Date(2026, 11, 15),
);

// ERROR: Cannot assign to 'born' because it is a read-only property.
joe.born = new Date(1967, 1, 1);

Sichtbarkeit (Visibility)

Klassen dienen der Kapselung. Hierzu ist es sinnvoll, wenn die Daten nicht einfach beliebig von aussen modifizierbar sind. Hierzu können Eigenschaften und Methoden mit einer bestimmten Sichtbarkeit versehen werden. Hierzu gibt es drei Optionen:

SichtbarkeitZugriff auf Eigenschaft/Methode
publicvon aussen möglich
privatenur von innerhalb der Klasse möglich
protectedvon innerhalb derselben und erbenden Klassen möglich

Das protected-Schlüsselwort wird beim Thema Vererbung genauer angeschaut. Hier sollen nur public und private von Interesse sein.

Wird keine Sichtbarkeit angegeben, gilt standardmässig public.

Die Eigenschaften der Klasse Person sollen privat gemacht werden. Bei den Methoden soll show() explizit öffentlich bleiben, während formatYMD nur noch von innen sichtbar, d.h. private sein soll.

class Person {
  private firstName: string = "";
  private middleName?: string;
  private lastName: string = "";
  private readonly born: Date;
  private died?: Date;

  constructor(
    firstName: string,
    lastName: string,
    born: Date,
    middleName?: string,
    died?: Date,
  ) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.born = born;
    this.middleName = middleName;
    this.died = died;
  }

  public show(): string {
    let fullName: string;
    if (this.middleName) {
      fullName = `${this.firstName} ${this.middleName} ${this.lastName}`;
    } else {
      fullName = `${this.firstName} ${this.lastName}`;
    }
    let lifeSpan: string;
    if (this.died) {
      const cross = "\u{1f547}";
      lifeSpan = `* ${this.formatYMD(this.born)}, ${cross} ${this.formatYMD(this.died!)}`;
    } else {
      lifeSpan = `* ${this.formatYMD(this.born)}`;
    }
    return `${fullName} (${lifeSpan})`;
  }

  private formatYMD(d: Date) {
    return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
  }
}

TypeScript erlaubt es, die Eigenschaften gleich in der Parameterliste des Konstruktors zu definieren, indem man das Schlüsselwort private (bzw. public) dort aufführt:

class Person {
  constructor(
    private firstName: string,
    private lastName: string,
    private born: Date,
    private middleName?: string,
    private died?: Date,
  ) {}
  // ...
}

Der Direktzugriff auf die Eigenschaften ist nun nicht mehr erlaubt:

const joe: Person = new Person(
  "Joe",
  "Doe",
  new Date(1968, 12, 23),
  "M.",
  new Date(2026, 11, 15),
);

// OK
console.log(joe.show());

// ERROR: Property 'firstName' is private and only accessible within class 'Person'.
console.log(joe.firstName, joe.lastName);
Info

Die Sichtbarkeiten public, private und protected sind TypeScript-Features. In JavaScript können Eigenschaften mit dem Präfix # ebenfalls privat gemacht werden:

class Person {
  #firstName;
  #lastName;
  constructor(firstName, lastName) {
    this.#firstName = firstName;
    this.#lastName = lastName;
  }
  fullName() {
    return `${this.#firstName} ${this.#lastName}`;
  }
}

const joe = new Person("Joe", "Doe");
console.log(joe.fullName()); // ok
console.log(joe.#firstName, joe.#lastName); // ERROR

Das Präfix # wird dadurch zum Teil des Namens der Eigenschaft! Im Modul 320 soll dieser Mechanismus nicht zum Einsatz kommen. Es sollen stattdessen die TypeScript-Sichtbarkeiten verwendet werden.

Akzessoren (Accessors: Getter & Setter)

Oftmals soll der Zugriff auf Datenelemente nicht ganz verboten sondern eingeschränkt werden. So möchte man beim Setzen eines Wertes eine Validierung des neuen Wertes durchführen, oder Berechnungen beim Auslesen eines Wertes durchführen. Zwar ist dies auch mit Methoden möglich, im Gebrauch sind aber sogenannte Akzessoren (engl. Accessors) etwas ergonomischer.

Man unterscheidet zwischen get- und set-Akzessoren. Für eine Eigenschaft können beide Varianten oder nur eine davon umgesetzt werden:

class Person {
  constructor(
    private firstName: string,
    private lastName: string,
    private born: Date,
    private middleName?: string,
    private died?: Date,
  ) {}

  get fullName(): string {
    let fullName: string;
    if (this.middleName) {
      fullName = `${this.firstName} ${this.middleName} ${this.lastName}`;
    } else {
      fullName = `${this.firstName} ${this.lastName}`;
    }
    return fullName;
  }

  set deathDate(d: Date) {
    if (d < this.born) {
      throw new Error(`stillborn`);
    } else {
      this.died = d;
    }
  }
  // ...
}

Akzessoren dürfen nicht den gleichen Namen wie Eigenschaften tragen. Ihr Zugriff erfolgt ohne runde Klammern:

const joe: Person = new Person("Joe", "Doe", new Date(1968, 12, 23), "M.");
console.log(joe.fullName); // NOT joe.fullName()
joe.deathDate = new Date(2026, 9, 10); // NOT joe.deathDate(...)
console.log(joe.show());

Lebenszyklen (statisch und instanzgebunden)

Eine Klasse definiert einen Datentyp. Das Schlüsselwort this greift nicht auf die Klasse zu, sondern auf die jeweilige Instanz der Klasse, d.h. auf ein spezifisches Objekt. Diese Instanzen entstehen aber erst durch die Initialisierung.

Es ist aber auch möglich, eine Klasse mit Daten auszustatten, die nicht für einzelne Instanzen gelten, sondern für die Klasse selber. Solche Eigenschaften bezeichnet man als statisch:

class Person {
  private static count: number = 0;

  constructor(
    private firstName: string,
    private lastName: string,
    private born: Date,
    private middleName?: string,
    private died?: Date,
  ) {
    Person.count++;
  }

  static get numberOfPeople() {
    return Person.count;
  }
  // ...
}

Die Eigenschaft count wird mit dem Schlüsselwort static deklariert und mit dem Wert 0 initialisiert. Im Konstruktor wird der Zähler inkrementiert. Dabei erfolgt der Zugriff über den Klassennamen Person, nicht über this, denn count ist an die Klasse und nicht an eine Instanz gebunden!

Der Getter numberOfPeople ist ebenfalls als statisch deklariert. Auch Methoden können statisch deklariert werden.

Der Zugriff erfolgt wiederum über den Klassennamen und nicht über den Objektnamen:

const joe: Person = new Person("Joe", "Doe", new Date(1968, 12, 23), "M.");
const jane: Person = new Person("Jane", "Done", new Date(1973, 10, 30));
console.log(Person.numberOfPeople); // 2

Statische Eigenschaften und Methoden sind immer dann sinnvoll, wenn man Informationen verwalten möchte, die nicht an eine bestimmte Instanz gebunden sind. Erinnern wir uns an die Methode formatYMD:

class Person {
  // ...
  private formatYMD(d: Date) {
    return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
  }
}

Da diese nur auf dem Argument d arbeitet und keinerlei Referenzen auf konkrete Instanzen mit this aufweist, könnte diese Methode auch als statisch definiert werden:

class Person {
  // ...
  private static formatYMD(d: Date) {
    return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
  }
}

Der Zugriff auf formatYMD aus der Methode show() heraus darf aber dann nicht mehr mit this, sondern nur noch über den Klassennamen Person erfolgen:

class Person {
  // ...
  public show(): string {
    let fullName: string;
    if (this.middleName) {
      fullName = `${this.firstName} ${this.middleName} ${this.lastName}`;
    } else {
      fullName = `${this.firstName} ${this.lastName}`;
    }
    let lifeSpan: string = `* ${Person.formatYMD(this.born)}`;
    if (this.died) {
      lifeSpan += `, \u{1f547} ${Person.formatYMD(this.died!)}`;
    }
    return `${fullName} (${lifeSpan})`;
  }
  // ...
}

Statische Methoden sind immer dann sinnvoll, wenn eine Funktion nur im Kontext einer bestimmten Klasse verwendet wird, aber nicht auf die Instanz zugreift, sondern nur auf ihre übergebenen Argumente – und auf statische Eigenschaften oder Akzessoren.