Zum Inhalt springen
aviral gupta

// B5.1 · ca. 30 Min. · Einstieg

Optionale und readonly-Eigenschaften

Nach dieser Lektion geben Sie optionalen Eigenschaften Defaults dort, wo Sie sie destrukturieren, machen Eigenschaften readonly und können sagen, was readonly schützt und was nicht.

Lektion 1 von 5 in B5 Objekttypen

Anfang des Moduls

Danach können Sie

  • Optionalen Eigenschaften in einem Destructuring-Muster Defaults geben und eine fehlende Eigenschaft von einer auf undefined gesetzten unterscheiden
  • Eigenschaften readonly machen, error TS2540 lesen und erklären, warum readonly flach ist
  • Erklären, warum sich eine readonly-Eigenschaft über eine andere Referenz und zur Laufzeit trotzdem ändern kann
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen mit JavaScript-Destructuring: Welche Defaults werden verwendet? Was gibt das aus?

    const { a = 1, b = 2, c = 3 } = { a: undefined, b: null };
    console.log(a, b, c);
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen: resident ist readonly. Welche Zeile lehnt tsc ab?

    interface Home {
      readonly resident: { name: string; age: number };
    }
    
    function visitForBirthday(home: Home) {
      home.resident.age++;
    }
    
    function evict(home: Home) {
      home.resident = { name: "Victor the Evictor", age: 42 };
    }
  3. Üben · Aufgabe 3 von 7

    Füllen Sie die Lücke so, dass xPos wie yPos den Default 0 hat und das Programm circle at 0,0 und dann square at 100,0 ausgibt.

    interface PaintOptions {
      shape: string;
      xPos?: number;
      yPos?: number;
    }
    
    function paintShape({ shape, xPos ____, yPos = 0 }: PaintOptions) {
      console.log(shape + " at " + xPos + "," + yPos);
    }
    
    paintShape({ shape: "circle" });
    paintShape({ shape: "square", xPos: 100 });
    function paintShape({ shape, xPos , yPos = 0 }: PaintOptions) {
  4. Üben · Aufgabe 4 von 7

    Beide Eigenschaften sind optional, aber nur fontSize bekommt einen Default. Welche Typen haben theme und fontSize beim Kommentar?

    interface Settings {
      theme?: string;
      fontSize?: number;
    }
    
    function describe(settings: Settings) {
      const { theme, fontSize = 14 } = settings;
      // here
    }
  5. Üben · Aufgabe 5 von 7

    Ein Projekt schaltet exactOptionalPropertyTypes in seiner tsconfig.json ein (die Kurs-tsconfig tut das nicht, und dort hat diese Datei keine Fehler). Welche Zeile lehnt tsc dann ab?

    interface UserDefaults {
      colorThemeOverride?: "dark" | "light";
    }
    
    const settings: UserDefaults = {};
    settings.colorThemeOverride = "dark";
    settings.colorThemeOverride = undefined;
    delete settings.colorThemeOverride;
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe, das Beispiel aus dem Handbook. Was passiert, wenn Sie npx tsc und dann node main.ts ausführen?

    interface Person {
      name: string;
      age: number;
    }
    
    interface ReadonlyPerson {
      readonly name: string;
      readonly age: number;
    }
    
    let writablePerson: Person = { name: "Person McPersonface", age: 42 };
    let readonlyPerson: ReadonlyPerson = writablePerson;
    
    console.log(readonlyPerson.age);
    writablePerson.age++;
    console.log(readonlyPerson.age);
  7. Anwenden · Aufgabe 7 von 7

    Kleine Aufgabe. Schreiben Sie ein Interface ShopConfig mit einem readonly shopName (ein String) und zwei optionalen Eigenschaften: currency (ein String) und taxPercent (eine Zahl). Schreiben Sie describeShop, das seinen Parameter mit den Defaults EUR und 19 destrukturiert und einen Text wie Corner Shop (EUR, tax 19%) zurückgibt. Rufen Sie es einmal nur mit shopName und einmal mit taxPercent: 7 auf. Versuchen Sie dann config.shopName = "Other" und lesen Sie den Fehler. Prüfen Sie es im Editor oder mit npx tsc und node main.ts.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Eine Bestellung mit Defaults und readonly-Eigenschaften

describeOrder destrukturiert seinen Parameter und gibt quantity und giftWrap Defaults, sodass der Rumpf keine Prüfungen auf undefined braucht. id und customer sind readonly. Danach ändert das Programm die Stadt des Kunden, was readonly erlaubt, weil es nur eine Ebene tief wirkt. Führen Sie npx tsc und dann node main.ts aus. Fügen Sie danach order.id = "X"; oder order.customer = order.customer; hinzu und führen Sie tsc erneut aus.

main.ts

// An order: optional settings get defaults, the id and the customer are readonly.
interface Customer {
  name: string;
  city: string;
}

interface Order {
  readonly id: string;
  readonly customer: Customer;
  quantity?: number;
  giftWrap?: boolean;
}

// Defaults in the pattern: in the body, quantity is a number and giftWrap a boolean.
function describeOrder({ id, customer, quantity = 1, giftWrap = false }: Order): string {
  const wrap = giftWrap ? ", gift-wrapped" : "";
  return id + ": " + quantity + " for " + customer.name + " in " + customer.city + wrap;
}

const order: Order = { id: "A-17", customer: { name: "Ada", city: "Bonn" } };
console.log(describeOrder(order));

// readonly is shallow: order.customer cannot point elsewhere, but its city can change.
order.customer.city = "Berlin";
console.log(describeOrder({ ...order, quantity: 3, giftWrap: true }));

// An explicit undefined gets the default too.
console.log(describeOrder({ id: "B-2", customer: order.customer, quantity: undefined }));

Ausführen mit

npx tsc
node main.ts

Ausgabe

A-17: 1 for Ada in Bonn
A-17: 3 for Ada in Berlin, gift-wrapped
B-2: 1 for Ada in Berlin
  • Für Aufrufer sind quantity und giftWrap optional; in describeOrder sind sie eine Zahl und ein Boolean.
  • order.id = "X" wäre error TS2540: Cannot assign to 'id' because it is a read-only property.
  • order.customer.city = "Berlin" kompiliert: customer ist readonly, city nicht.
  • quantity: undefined bekommt den Default 1, genau wie eine fehlende quantity.
Ändern und ausführen

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den TypeScript-Compiler herunter (bis zu 2.1 MB) und speichert ihn im Cache. Ihr Code bleibt auf Ihrem Gerät.

Übungen

Übung 1 von 2

Defaults statt Prüfungen auf undefined

formatPrice soll für { amount: 4.5 } 4.50 EUR liefern: currency hat den Default eur, in Großbuchstaben geschrieben, und decimals den Default 2. tsc meldet error TS18048: 'options.currency' is possibly 'undefined'. Destrukturieren Sie stattdessen den Parameter und geben Sie currency und decimals ihre Defaults im Muster. decimals: 0 muss 0 bleiben. Führen Sie die Tests aus: Der erste ist die Typprüfung.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den TypeScript-Compiler herunter (bis zu 2.1 MB) und speichert ihn im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Ersetzen Sie den Parameter options: PriceOptions durch ein Muster: ({ amount, currency, decimals }: PriceOptions).

  2. Hinweis 2

    Ein Default steht im Muster hinter dem Namen, mit =: currency = "eur".

  3. Hinweis 3

    Verwenden Sie die Variablen im Rumpf: amount.toFixed(decimals) + " " + currency.toUpperCase().

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

export interface PriceOptions {
  amount: number;
  currency?: string;
  decimals?: number;
}

export function formatPrice({ amount, currency = "eur", decimals = 2 }: PriceOptions): string {
  return amount.toFixed(decimals) + " " + currency.toUpperCase();
}
Auf dem eigenen Computer ausführen

Installieren Sie TypeScript 7.0 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.ts

export interface PriceOptions {
  amount: number;
  currency?: string;
  decimals?: number;
}

export function formatPrice(options: PriceOptions): string {
  return options.amount.toFixed(options.decimals) + " " + options.currency.toUpperCase();
}

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {formatPrice} from './main.ts';

test('beide Defaults werden verwendet', () => {
  const got = formatPrice({amount: 4.5});
  assert.equal(got, '4.50 EUR', `formatPrice({amount: 4.5}) ergab ${JSON.stringify(got)}`);
});

test('eine angegebene currency und decimals werden verwendet', () => {
  const got = formatPrice({amount: 3, currency: 'usd', decimals: 1});
  assert.equal(got, '3.0 USD', `formatPrice({amount: 3, currency: 'usd', decimals: 1}) ergab ${JSON.stringify(got)}`);
});

test('decimals: 0 bleibt 0', () => {
  const got = formatPrice({amount: 7, decimals: 0});
  assert.equal(got, '7 EUR', `formatPrice({amount: 7, decimals: 0}) ergab ${JSON.stringify(got)}`);
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc braucht TypeScript im Ordner: Führen Sie dort einmal npm install --save-dev typescript aus. tsc prüft die Typen; Node.js führt main.ts aus, indem es sie entfernt.

Programm ausführen:

npx tsc
node main.ts

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

npx tsc
node --test
learnrun.js herunterladen

Übung 2 von 2

Feste ids und ein Kunde, der umzieht

Eine Bestellung muss ihre id und ihr Kundenobjekt behalten; andere Teile des Shops halten dasselbe Kundenobjekt und müssen eine neue Stadt sehen. forbiddenChanges prüft den ersten Teil: Jedes // @ts-expect-error erwartet einen Fehler in der nächsten Zeile, und tsc meldet error TS2578: Unused '@ts-expect-error' directive., solange die Zuweisungen noch kompilieren. Machen Sie id und customer readonly. Korrigieren Sie dann moveCustomer, das tsc jetzt ablehnt: Ändern Sie die Stadt des Kundenobjekts, das die Bestellung schon hat.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den TypeScript-Compiler herunter (bis zu 2.1 MB) und speichert ihn im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    readonly steht im Interface vor dem Eigenschaftsnamen: readonly id: string;

  2. Hinweis 2

    Danach ist order.customer = { … } in moveCustomer error TS2540. readonly ist flach: Die Stadt in customer kann sich weiterhin ändern.

  3. Hinweis 3

    Der Rumpf von moveCustomer wird zu order.customer.city = city;

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

export interface Order {
  readonly id: string;
  readonly customer: { name: string; city: string };
  total: number;
}

export function moveCustomer(order: Order, city: string): void {
  order.customer.city = city;
}

export function addToTotal(order: Order, amount: number): void {
  order.total += amount;
}

function forbiddenChanges(order: Order) {
  // @ts-expect-error: an order keeps its id
  order.id = "X-0";
  // @ts-expect-error: an order keeps its customer object
  order.customer = { name: "Mallory", city: "Nowhere" };
}
Auf dem eigenen Computer ausführen

Installieren Sie TypeScript 7.0 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.ts

export interface Order {
  id: string;
  customer: { name: string; city: string };
  total: number;
}

export function moveCustomer(order: Order, city: string): void {
  order.customer = { name: order.customer.name, city };
}

export function addToTotal(order: Order, amount: number): void {
  order.total += amount;
}

function forbiddenChanges(order: Order) {
  // @ts-expect-error: an order keeps its id
  order.id = "X-0";
  // @ts-expect-error: an order keeps its customer object
  order.customer = { name: "Mallory", city: "Nowhere" };
}

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {moveCustomer, addToTotal} from './main.ts';

test('moveCustomer setzt die neue Stadt', () => {
  const order = {id: 'A-1', customer: {name: 'Ada', city: 'Bonn'}, total: 10};
  moveCustomer(order, 'Berlin');
  assert.equal(order.customer.city, 'Berlin', `die Stadt ist ${order.customer.city}`);
});

test('moveCustomer behält dasselbe Kundenobjekt', () => {
  const customer = {name: 'Ada', city: 'Bonn'};
  const order = {id: 'A-1', customer, total: 10};
  moveCustomer(order, 'Berlin');
  assert.ok(order.customer === customer, 'order.customer wurde durch ein neues Objekt ersetzt');
  assert.equal(customer.city, 'Berlin', `das gemeinsame Kundenobjekt sagt noch ${customer.city}`);
});

test('addToTotal addiert zur Summe', () => {
  const order = {id: 'A-1', customer: {name: 'Ada', city: 'Bonn'}, total: 10};
  addToTotal(order, 2.5);
  assert.equal(order.total, 12.5, `die Summe ist ${order.total}`);
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc braucht TypeScript im Ordner: Führen Sie dort einmal npm install --save-dev typescript aus. tsc prüft die Typen; Node.js führt main.ts aus, indem es sie entfernt.

Programm ausführen:

npx tsc
node main.ts

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

npx tsc
node --test
learnrun.js herunterladen

Häufige Fehler

Schreiben in eine readonly-Eigenschaft

interface Config {
  readonly apiUrl: string;
  retries: number;
}

const config: Config = { apiUrl: "https://example.com", retries: 3 };
config.retries = 5;
config.apiUrl = "http://localhost";

Was tsc oder Node.js ausgibt

main.ts(8,8): error TS2540: Cannot assign to 'apiUrl' because it is a read-only property.

Warum, und die Lösung

Der Typ sagt, dass apiUrl einmal beim Erzeugen des Objekts gesetzt und nie geändert wird. Brauchen Sie eine andere URL, erzeugen Sie ein neues Objekt: const local: Config = { ...config, apiUrl: "http://localhost" }. Muss sie sich wirklich an Ort und Stelle ändern, sollte die Eigenschaft nicht readonly sein: Entfernen Sie den Modifier, statt ihn zu umgehen.

Eine destrukturierte optionale Eigenschaft ohne Default

interface Settings {
  theme?: string;
  fontSize?: number;
}

function describe(settings: Settings) {
  const { theme, fontSize = 14 } = settings;
  return theme.toUpperCase() + " " + fontSize + "px";
}

Was tsc oder Node.js ausgibt

main.ts(8,10): error TS18048: 'theme' is possibly 'undefined'.

Warum, und die Lösung

Destructuring entfernt undefined nicht: theme ist string | undefined, wie settings.theme es wäre. fontSize ist nur deshalb eine Zahl, weil es einen Default hat. Geben Sie theme auch einen, const { theme = "light", fontSize = 14 } = settings, oder prüfen Sie es auf undefined, bevor Sie toUpperCase aufrufen.

Aufruf ohne Argument, wenn nur die Eigenschaften Defaults haben

interface Options {
  host?: string;
  port?: number;
}

function connect({ host = "localhost", port = 80 }: Options) {
  return host + ":" + port;
}

console.log(connect());

Was tsc oder Node.js ausgibt

main.ts(10,13): error TS2554: Expected 1 arguments, but got 0.

Warum, und die Lösung

Die Defaults gehören zu den Eigenschaften des Objekts, aber das Objekt selbst ist weiterhin ein Pflichtparameter, und undefined zu destrukturieren würde einen TypeError werfen. Geben Sie auch dem ganzen Parameter einen Default: function connect({ host = "localhost", port = 80 }: Options = {}). Dann verwendet connect() ein leeres Objekt, und beide Eigenschafts-Defaults greifen.

TypeScript im Browser: der Compiler von TypeScript 6.0.3, Apache-2.0, danach die eigene Engine Ihres Browsers. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Defaults für optionale Eigenschaften, im Muster

In B2.3 haben Sie optionale Eigenschaften vorher auf undefined geprüft. Ein Default in einem Destructuring-Muster erledigt das für Sie: function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions). Aufrufer dürfen xPos weiterhin weglassen, im Rumpf ist es aber eine Zahl. Ohne Default ergibt const { theme } = settings den Typ string | undefined. Ein Default ersetzt nur undefined, quantity: 0 bleibt also 0. Für einen Aufruf ganz ohne Argument bekommt auch der ganze Parameter einen Default: ({ port = 80 }: Options = {}); sonst ist connect() error TS2554. theme?: string nimmt auch theme: undefined an, obwohl "theme" in settings dann true ergibt. Die Option exactOptionalPropertyTypes, die strict nicht einschaltet, lehnt das mit error TS2375 ab.

readonly verbietet das Schreiben einer Eigenschaft, eine Ebene tief

readonly vor einem Eigenschaftsnamen verbietet Zuweisungen: interface Config { readonly apiUrl: string; retries: number }. Lesen ist erlaubt, ebenso ein Wert beim Erzeugen des Objekts, aber config.apiUrl = "…" ist error TS2540: Cannot assign to 'apiUrl' because it is a read-only property. delete config.apiUrl ist error TS2704. Das ist nicht const: const legt eine Variable fest, readonly eine Eigenschaft, und const p = { x: 1 } erlaubt weiterhin p.x = 2. readonly ist flach. Mit readonly resident: { name: string; age: number } ist home.resident = { … } TS2540, aber home.resident.age++ kompiliert: Die Eigenschaft kann nicht auf ein anderes Objekt zeigen, das Objekt selbst kann sich aber ändern.

readonly gehört zum Typ, nicht zum Objekt

Das Handbook: „TypeScript doesn’t factor in whether properties on two types are readonly when checking whether those types are compatible“. Also kompiliert let readonlyPerson: ReadonlyPerson = writablePerson, und nach writablePerson.age++ gibt readonlyPerson.age 43 statt 42 aus: zwei Namen, ein Objekt. Umgekehrt kompiliert es auch: Eine Funktion, die eine Person annimmt, darf die ReadonlyPerson ändern, die Sie ihr übergeben. Und zur Laufzeit bleibt von readonly nichts übrig. Node.js entfernt es, ein Schreibzugriff, den tsc abgelehnt hat, findet also trotzdem statt, wenn Sie die Datei dennoch ausführen. Lesen Sie readonly als Versprechen über einen Namen: Code, der diesen Namen verwendet, ändert die Eigenschaft nicht. Wer eine veränderbare Referenz hat, kann es weiterhin.

Quellen

Zuletzt geprüft am 4. Oktober 2026