Zum Inhalt springen
aviral gupta

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

Array<T>, ReadonlyArray und Tupel

Nach dieser Lektion schützen Sie Arrays mit readonly vor Änderungen und typisieren Daten mit festen Positionen als Tupel.

Lektion 4 von 5 in B5 Objekttypen

Danach können Sie

  • Array-Typen als T[] oder Array<T> schreiben und Änderungen mit readonly T[] oder ReadonlyArray<T> verbieten
  • Erklären, warum ein veränderbares Array zu einem readonly-Array passt, aber nicht umgekehrt, und error TS4104 beheben
  • Feste Positionen mit Tupeln typisieren, auch mit optionalen, Rest- und readonly-Elementen und as const
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen mit JavaScript: copy ist ein weiterer Name für scores. Was gibt das aus?

    const scores = [70, 85];
    const copy = scores;
    copy.push(99);
    console.log(scores.length);
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es voraus, bevor Sie weiterlesen: scores ist ein readonly-Array. Welche Zeile lehnt tsc ab?

    function report(scores: readonly number[]): number {
      const best = scores.toSorted((a, b) => b - a);
      best.push(0);
      scores.push(0);
      return best[0];
    }
  3. Üben · Aufgabe 3 von 7

    Füllen Sie die Lücke so, dass total verspricht, die Preise nicht zu ändern: Die Zeile mit // @ts-expect-error erwartet einen Fehler, und das Programm gibt 5.5 aus.

    function total(prices: ____ number[]): number {
      // @ts-expect-error: total must not change the prices
      prices.push(0);
      let sum = 0;
      for (const p of prices) sum += p;
      return sum;
    }
    
    console.log(total([2, 3.5]));
    function total(prices: number[]): number {
  4. Üben · Aufgabe 4 von 7

    Zwei Zuweisungen zwischen einem veränderbaren und einem readonly-Array. Welche Zeile lehnt tsc ab?

    let editable: string[] = ["a", "b"];
    let locked: readonly string[] = ["c"];
    
    locked = editable;
    editable = locked;
  5. Üben · Aufgabe 5 von 7

    Die Koordinate aus dem Handbook mit optionalem drittem Element. Welche Typen haben z und n am Kommentar?

    function setCoordinate(coord: [number, number, number?]) {
      const [x, y, z] = coord;
      const n = coord.length;
      // here
      return x + y + (z ?? 0) + n;
    }
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. pair ist ein Tupel mit genau zwei Elementen. Was passiert, wenn Sie npx tsc und dann node main.ts ausführen?

    const pair: [string, number] = ["Ada", 36];
    pair.push(1815);
    console.log(pair.length, pair);
  7. Anwenden · Aufgabe 7 von 7

    Kleine Aufgabe. Schreiben Sie minMax(values), die ein readonly number[] nimmt und den kleinsten und größten Wert als readonly [number, number] zurückgibt. Rufen Sie sie mit einem gewöhnlichen Array wie [11.5, 19, 16.5] auf, zerlegen Sie das Ergebnis in low und high und geben Sie 11.5 to 19 aus. Probieren Sie dann values.push(0) in minMax und eine Zuweisung an das erste Element des Ergebnisses aus, und lesen Sie beide 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

Temperaturmessungen

Ein Reading ist ein readonly-Tupel: zuerst die Uhrzeit, dann die Temperatur. Die Liste der Messungen ist ebenfalls readonly, und warmest verspricht in seinem Parameter, sie nicht zu ändern, sortiert also mit toSorted eine Kopie. range nimmt ein Tupel, dessen Einheit fehlen darf; week hat ein Label, gefolgt von beliebig vielen Werten; units entsteht mit as const. Führen Sie npx tsc aus, dann node main.ts. Fügen Sie dann readings.push(["21:00", 14]); und week[0] = 1; hinzu und führen Sie tsc erneut aus.

main.ts

// A reading has two fixed positions: the time, then the temperature.
type Reading = readonly [string, number];

// Once loaded, the readings must not change: readonly on the array too.
const readings: readonly Reading[] = [
  ["06:00", 11.5],
  ["12:00", 19],
  ["18:00", 16.5]
];

// readonly in the parameter: this function promises not to change the list.
function warmest(list: readonly Reading[]): Reading {
  const sorted = list.toSorted((a, b) => b[1] - a[1]); // a new, mutable copy
  return sorted[0];
}

// An optional last element: the unit may be left out.
function range(r: [number, number, string?]): string {
  const [min, max, unit = "C"] = r;
  return min + " to " + max + " " + unit + " (" + r.length + " elements)";
}

// A rest element: a label, then any number of values.
const week: [string, ...number[]] = ["Mon", 11.5, 19, 16.5];
const [label, ...values] = week;

// as const: a readonly tuple of literal types.
const units = ["C", "F"] as const;

const [time, temp] = warmest(readings);
console.log("warmest: " + temp + " at " + time);
console.log(range([11.5, 19]));
console.log(range([52.7, 66.2, "F"]));
console.log(label + ": " + values.length + " values");
console.log(units.join(" or "), readings.length);

Ausführen mit

npx tsc
node main.ts

Ausgabe

warmest: 19 at 12:00
11.5 to 19 C (2 elements)
52.7 to 66.2 F (3 elements)
Mon: 3 values
C or F 3
  • toSorted gibt ein neues Array zurück, also lässt warmest die Reihenfolge von readings unverändert.
  • Das Zerlegen eines Tupels gibt jedem Teil seinen eigenen Typ: time ist ein String, temp eine Zahl.
  • r.length ist 2 oder 3, je nachdem, ob die Einheit übergeben wurde.
  • readings.push(…) wäre error TS2339, weil readings readonly ist.
Ä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

Ein Median, der die Eingabe in Ruhe lässt

median sortiert das übergebene Array, also ändert sich die Reihenfolge im Array des Aufrufers, und das // @ts-expect-error in noEdits ist unbenutzt, weil Values push erlaubt. Machen Sie Values zu einem readonly-Array von Zahlen. tsc lehnt dann values.sort ab: Sortieren Sie stattdessen mit toSorted eine Kopie. Behandeln Sie außerdem eine gerade Anzahl von Werten: Der Median ist dann der Durchschnitt der beiden mittleren Werte, median([4, 1, 3, 2]) ist also 2.5. 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

    Der Modifikator readonly steht vor dem Array-Typ: export type Values = readonly number[];

  2. Hinweis 2

    toSorted funktioniert wie sort, gibt aber ein neues Array zurück: const sorted = values.toSorted((a, b) => a - b);

  3. Hinweis 3

    Mit mid = Math.floor(sorted.length / 2) nimmt eine gerade Länge (sorted[mid - 1] + sorted[mid]) / 2.

Eine Lösung zeigen

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

export type Values = readonly number[];

export function median(values: Values): number {
  const sorted = values.toSorted((a, b) => a - b);
  const mid = Math.floor(sorted.length / 2);
  if (sorted.length % 2 === 0) return (sorted[mid - 1] + sorted[mid]) / 2;
  return sorted[mid];
}

function noEdits(values: Values) {
  // @ts-expect-error: Values is read-only
  values.push(0);
}
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 type Values = number[];

export function median(values: Values): number {
  values.sort((a, b) => a - b);
  return values[Math.floor(values.length / 2)];
}

function noEdits(values: Values) {
  // @ts-expect-error: Values is read-only
  values.push(0);
}

main.test.ts

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

test('der Median einer ungeraden Anzahl von Werten ist der mittlere', () => {
  const got = median([5, 1, 3]);
  assert.equal(got, 3, `median([5, 1, 3]) ergab ${got}`);
});

test('der Median einer geraden Anzahl von Werten ist der Durchschnitt der beiden mittleren', () => {
  const got = median([4, 1, 3, 2]);
  assert.equal(got, 2.5, `median([4, 1, 3, 2]) ergab ${got}`);
});

test('median lässt die Reihenfolge im Array des Aufrufers unverändert', () => {
  const values = [5, 1, 3];
  median(values);
  assert.deepEqual(values, [5, 1, 3], `nach median ist das Array ${JSON.stringify(values)}`);
});

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

Punktezeilen als Tupel

Eine Row ist der Name eines Spielers, gefolgt von beliebig vielen Punktzahlen, aber (string | number)[] sagt nicht, was zuerst kommt, also ergibt das Zerlegen string | number, und tsc meldet error TS2322 für player. Machen Sie Row zu einem Tupel mit Rest-Element, sodass das // @ts-expect-error bei der leeren Zeile seinen Fehler findet. Lassen Sie bestScore ein readonly [string, number] zurückgeben, sodass auch das Schreiben in top[1] gemeldet wird. Ein Spieler ohne Punktzahlen bekommt 0 als beste Punktzahl.

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

    Ein Rest-Element steht als letztes in den Klammern: [string, ...number[]] ist ein String, gefolgt von beliebig vielen Zahlen.

  2. Hinweis 2

    Setzen Sie readonly vor den Rückgabetyp: function bestScore(row: Row): readonly [string, number] {

  3. Hinweis 3

    Math.max() ohne Argumente ergibt -Infinity, prüfen Sie also zuerst scores.length === 0 und geben Sie [player, 0] zurück.

Eine Lösung zeigen

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

export type Row = [string, ...number[]];

export function bestScore(row: Row): readonly [string, number] {
  const [player, ...scores] = row;
  if (scores.length === 0) return [player, 0];
  return [player, Math.max(...scores)];
}

// @ts-expect-error: a row starts with the player's name
const empty: Row = [];

const top = bestScore(["Ada", 9]);
// @ts-expect-error: the result is read-only
top[1] = 10;
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 type Row = (string | number)[];

export function bestScore(row: Row): [string, number] {
  const [player, ...scores] = row;
  return [player, Math.max(...scores)];
}

// @ts-expect-error: a row starts with the player's name
const empty: Row = [];

const top = bestScore(["Ada", 9]);
// @ts-expect-error: the result is read-only
top[1] = 10;

main.test.ts

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

test('die beste Punktzahl kommt zusammen mit dem Spieler', () => {
  const got = bestScore(['Ada', 3, 9, 4]);
  assert.deepEqual(got, ['Ada', 9], `bestScore(['Ada', 3, 9, 4]) ergab ${JSON.stringify(got)}`);
});

test('negative Punktzahlen zählen auch', () => {
  const got = bestScore(['Cy', -2, -5]);
  assert.deepEqual(got, ['Cy', -2], `bestScore(['Cy', -2, -5]) ergab ${JSON.stringify(got)}`);
});

test('ein Spieler ohne Punktzahlen bekommt 0', () => {
  const got = bestScore(['Bob']);
  assert.deepEqual(got, ['Bob', 0], `bestScore(['Bob']) 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

Häufige Fehler

push auf einem readonly-Array aufrufen

const tags: readonly string[] = ["news", "tech"];
tags.push("sport");

Was tsc oder Node.js ausgibt

main.ts(2,6): error TS2339: Property 'push' does not exist on type 'readonly string[]'.

Warum, und die Lösung

Ein readonly-Array-Typ hat keine Methoden, die ihn ändern, push ist also schlicht nicht vorhanden. Muss die Liste wirklich wachsen, machen Sie sie zu einem string[]. Soll sie bleiben, wie sie ist, bauen Sie ein neues Array: const more = [...tags, "sport"]; oder tags.concat("sport"). Beides lässt tags unverändert und gibt Ihnen ein neues, veränderbares Array.

Ein as-const-Array einem veränderbaren Typ übergeben

const roles = ["admin", "editor"] as const;
const list: string[] = roles;

Was tsc oder Node.js ausgibt

main.ts(2,7): error TS4104: The type 'readonly ["admin", "editor"]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.

Warum, und die Lösung

as const macht roles zu einem readonly-Tupel, und ein string[] würde darauf push und sort erlauben. Lesen Sie die Liste nur, typisieren Sie sie ebenfalls als readonly string[]. Müssen Sie sie ändern, kopieren Sie sie: const list: string[] = [...roles];. Die Kopie ist ein neues Array, roles bleibt also, wie es war.

Erwarten, dass ein Array-Literal ein Tupel ist

const pair = ["Ada", 36];
const person: [string, number] = pair;

Was tsc oder Node.js ausgibt

main.ts(2,7): error TS2322: Type '(string | number)[]' is not assignable to type '[string, number]'.

Warum, und die Lösung

Ohne Tupeltyp inferiert tsc ein gewöhnliches Array, (string | number)[], das jede Länge und jede Reihenfolge von Strings und Zahlen haben könnte. Die nächste Zeile sagt es: Target requires 2 element(s) but source may have fewer. Annotieren Sie dort, wo der Wert entsteht, const pair: [string, number] = ["Ada", 36], oder nehmen Sie as const, wenn er sich nie ändert.

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

readonly T[]: ein Array, das Sie lesen, aber nicht ändern können

string[] ist die Kurzform von Array<string>; beide bedeuten dasselbe. Achten Sie bei Unions auf die Klammern: (string | number)[] enthält beide Arten, string | number[] ist ein String oder ein Array von Zahlen. readonly string[], in Langform ReadonlyArray<string>, entfernt jede Methode, die das Array ändert: names.push("x") ist error TS2339: Property 'push' does not exist on type 'readonly string[]'., ebenso sort, reverse und splice. names[0] = "x" ist error TS2542. Lesen, slice, map und toSorted funktionieren weiterhin, und die letzten drei geben ein neues, veränderbares Array zurück. Einen ReadonlyArray-Konstruktor gibt es nicht (TS2693). Wie readonly-Eigenschaften wirkt das nur eine Ebene tief und nur für tsc: Die Objekte darin können sich weiter ändern.

Veränderbar passt zu readonly, nicht umgekehrt

Ein Parameter vom Typ readonly number[] nimmt jedes Zahlen-Array an, denn die Funktion verspricht, es nicht zu ändern. Das Handbook: „we can pass any array into that function without worrying that it will change its contents.“ Die andere Richtung wird abgelehnt: Ein readonly string[] einem string[] zuzuweisen ist error TS4104: The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'., weil der neue Name push erlauben würde. Das ist der Unterschied zu readonly-Eigenschaften (B5.1), bei denen tsc beide Richtungen erlaubt. Brauchen Sie eine änderbare Kopie, erstellen Sie eine: values.slice() oder [...values]. readonly friert nichts ein: Das ursprüngliche Array kann sich unter einem anderen Namen weiter ändern.

Tupel: feste Positionen mit eigenen Typen

[string, number] ist ein Array mit genau zwei Elementen: erst ein String, dann eine Zahl. pair[2] ist error TS2493, und ["a", 1, 2] passt nicht (Source has 3 element(s) but target allows only 2.). Ein einfaches Array-Literal wird nie als Tupel inferiert: const pair = ["Ada", 36] ist (string | number)[]. Ein ? markiert optionale Elemente am Ende: [number, number, number?] hat die Länge 2 | 3. Ein Rest-Element erlaubt mehr: [string, ...number[]] ist ein String, gefolgt von beliebig vielen Zahlen. Ein Tupel hat weiterhin push, also nehmen Sie lieber readonly [string, number]: Dann ist pair[0] = "x" error TS2540. as const auf einem Array-Literal ergibt genau so ein readonly-Tupel.

Quellen

Zuletzt geprüft am 4. Oktober 2026