Zum Inhalt springen
aviral gupta

// B5.5 · ca. 38 Min. · Einstieg

Objekttypen im Zusammenspiel; Projekt: ein typisierter Bestandslader

Nach dieser Lektion modellieren Sie verschachtelte Daten mit allen Objekttyp-Werkzeugen dieses Moduls, laden sie sicher aus JSON und berechnen Auswertungen, ohne sie zu verändern.

Lektion 5 von 5 in B5 Objekttypen

Ende des Moduls

Danach können Sie

  • Verschachtelte Daten mit optionalen und readonly-Eigenschaften, einer Indexsignatur, einem erweiterten Interface, readonly-Arrays und einem Tupel modellieren
  • Einen unknown-Wert aus JSON.parse mit selbst geschriebenen Prüfungen und Type Predicates in ein typisiertes Objekt umwandeln
  • Auswertungen über readonly-Daten berechnen, ohne sie zu ändern, und sie mit node:test testen
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen mit JavaScript: JSON.parse liefert, was auch immer im Text steht. Was gibt dieser Code aus?

    const data = JSON.parse('{ "price": "25" }');
    console.log(data.price * 2, typeof data.price);
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es voraus, bevor Sie weiterlesen: Das JSON hat eine Zahl, wo Item einen String hat. Was passiert, wenn Sie npx tsc und dann node main.ts ausführen?

    interface Item {
      name: string;
    }
    
    const raw = JSON.parse('{ "items": [{ "name": 42 }] }');
    const items: Item[] = raw.items;
    console.log(items[0].name.toUpperCase());
  3. Üben · Aufgabe 3 von 7

    Setzen Sie den Typ für raw ein, sodass tsc raw.items ablehnt, bis es geprüft ist: Die Zeile // @ts-expect-error erwartet diesen Fehler.

    interface Inventory {
      items: string[];
    }
    
    const raw: ____ = JSON.parse('{ "items": ["lamp"] }');
    // @ts-expect-error: check raw before you use it
    console.log(raw.items);
    const raw: = JSON.parse('{ "items": ["lamp"] }');
  4. Üben · Aufgabe 4 von 7

    value ist zunächst unknown. Welchen Typ hat es beim Kommentar?

    function tagsOf(value: unknown) {
      if (Array.isArray(value)) {
        // here
        return value;
      }
      return [];
    }
  5. Üben · Aufgabe 5 von 7

    Das Modell eines Artikels. Welche Zeilen lehnt tsc ab?

    interface Product {
      readonly sku: string;
      name: string;
      note?: string;
    }
    interface Item extends Product {
      tags: readonly string[];
      location: readonly [string, number];
    }
    const lamp: Item = { sku: "L-1", name: "Lamp", tags: ["light"], location: ["A", 3] };
    lamp.name = "Desk lamp";
    lamp.note = "new";
    lamp.tags = [...lamp.tags, "sale"];
    lamp.location[1] = 4;
    lamp.sku = "L-2";
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. isRecord vergisst einen Fall. Was passiert, wenn Sie npx tsc und dann node main.ts ausführen?

    function isRecord(value: unknown): value is { [key: string]: unknown } {
      return typeof value === "object";
    }
    
    function skuOf(value: unknown): string {
      if (!isRecord(value)) throw new Error("not an object");
      const { sku } = value;
      return typeof sku === "string" ? sku : "no sku";
    }
    
    console.log(skuOf(JSON.parse("null")));
  7. Anwenden · Aufgabe 7 von 7

    Kleine Aufgabe. Schreiben Sie ein Product mit readonly sku, einem price und einem optionalen discount (einer Zahl). Schreiben Sie parseProduct(value: unknown), das das Objekt und seine Felder mit typeof und einer Hilfsfunktion fail(message): never prüft. Ein fehlender discount ist in Ordnung; jeder andere Nicht-Zahl-Wert scheitert mit discount must be a number. Laden Sie ein Produkt mit discount 5 und eines mit "5%". 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

Ein Bestandslader

Das ganze Modul in einem Programm. DATA ist der JSON-Text. Das Modell nutzt jedes Werkzeug aus B5. Die Predicates und parseItem prüfen einen unknown-Wert Feld für Feld; parseInventory prüft das äußere Objekt und sein Array items. totalStock und der Wert lesen die Daten und ändern nichts. Der letzte Aufruf lädt einen Artikel, dessen price "cheap" ist. Führen Sie npx tsc und dann node main.ts aus. Ändern Sie danach "B" in DATA in 2 und führen Sie es erneut aus.

main.ts

// The data, as it might arrive from a file or a server.
const DATA = `{
  "updated": "2026-10-04",
  "items": [
    { "sku": "L-1", "name": "Desk lamp", "price": 25, "location": ["A", 3],
      "tags": ["light"], "stock": { "berlin": 4, "bonn": 2 } },
    { "sku": "C-7", "name": "Chair", "price": 80, "note": "assembly needed",
      "location": ["B", 1], "tags": ["office", "seating"], "stock": { "berlin": 1 } }
  ]
}`;

// The model: every idea from module B5.
interface Product {
  readonly sku: string;
  name: string;
  price: number;
  note?: string;
}
interface Stock {
  readonly [warehouse: string]: number;
}
type Location = readonly [string, number]; // aisle, shelf
interface Item extends Product {
  location: Location;
  tags: readonly string[];
  stock: Stock;
}
interface Inventory {
  readonly updated: string;
  readonly items: readonly Item[];
}

// Checks: from unknown to the model, one property at a time.
function fail(message: string): never {
  throw new Error(message);
}
function isRecord(value: unknown): value is { [key: string]: unknown } {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isLocation(value: unknown): value is Location {
  return Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "number";
}
function isStringArray(value: unknown): value is readonly string[] {
  return Array.isArray(value) && value.every((v) => typeof v === "string");
}
function isStock(value: unknown): value is Stock {
  return isRecord(value) && Object.values(value).every((n) => typeof n === "number");
}

function parseItem(value: unknown): Item {
  if (!isRecord(value)) fail("an item must be an object");
  const { sku, name, price, note, location, tags, stock } = value;
  if (typeof sku !== "string") fail("sku must be a string");
  if (typeof name !== "string") fail("name must be a string");
  if (typeof price !== "number") fail("price must be a number");
  if (!isLocation(location)) fail("location must be [aisle, shelf]");
  if (!isStringArray(tags)) fail("tags must be strings");
  if (!isStock(stock)) fail("stock must map warehouses to numbers");
  const item: Item = { sku, name, price, location, tags, stock };
  if (note !== undefined) {
    if (typeof note !== "string") fail("note must be a string");
    item.note = note;
  }
  return item;
}

function parseInventory(text: string): Inventory {
  const raw: unknown = JSON.parse(text);
  if (!isRecord(raw)) fail("the inventory must be an object");
  const { updated, items } = raw;
  if (typeof updated !== "string") fail("updated must be a string");
  if (!Array.isArray(items)) fail("items must be an array");
  return { updated, items: items.map(parseItem) };
}

// Summaries: read the data, never change it.
function totalStock(item: Item): number {
  let total = 0;
  for (const count of Object.values(item.stock)) total += count;
  return total;
}

const inventory = parseInventory(DATA);
for (const item of inventory.items) {
  const [aisle, shelf] = item.location;
  console.log(item.sku + " " + item.name + ": " + totalStock(item) + " in stock, aisle " + aisle + " shelf " + shelf + (item.note ? " (" + item.note + ")" : ""));
}
let value = 0;
for (const item of inventory.items) value += item.price * totalStock(item);
console.log("value: " + value + ", updated " + inventory.updated);

try {
  parseInventory('{ "updated": "2026-10-04", "items": [{ "sku": "X-1", "name": "Shelf", "price": "cheap" }] }');
} catch (error) {
  console.log("rejected: " + (error instanceof Error ? error.message : String(error)));
}

Ausführen mit

npx tsc
node main.ts

Ausgabe

L-1 Desk lamp: 6 in stock, aisle A shelf 3
C-7 Chair: 1 in stock, aisle B shelf 1 (assembly needed)
value: 230, updated 2026-10-04
rejected: price must be a number
  • note wird nur gesetzt, wenn der Artikel eine hat, also hat die Lampe gar keine Eigenschaft note.
  • Object.values(item.stock) ist ein number[], also addiert die Schleife Zahlen.
  • items.map(parseItem) prüft jeden Artikel; der erste fehlerhafte beendet das Laden mit seiner Meldung.
  • Mit 2 statt "B" stoppt das erste parseInventory mit Error: location must be [aisle, shelf], bevor etwas ausgegeben wird: Der Stuhl besteht isLocation nicht.
Ä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 3

Schritt 1: einen Artikel modellieren

Korrigieren Sie das Modell, sodass lamp dazu passt und jedes // @ts-expect-error in noEdits seinen Fehler findet. note wird optional. stock wird ein exportiertes Interface Stock mit einer readonly-Indexsignatur: Lagernamen auf Bestände. tags wird ein readonly string[], location ein readonly-Tupel aus Gang und Regal. Lassen Sie dann totalStock die Bestände aller Lager addieren, sodass die Lampe auf 6 kommt. 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

    export interface Stock { readonly [warehouse: string]: number; } und dann stock: Stock in Item.

  2. Hinweis 2

    readonly steht vor beiden Array-Typen: tags: readonly string[]; location: readonly [string, number];

  3. Hinweis 3

    let total = 0; for (const count of Object.values(item.stock)) total += count; return total;

Eine Lösung zeigen

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

export interface Product {
  readonly sku: string;
  name: string;
  price: number;
  note?: string;
}

export interface Stock {
  readonly [warehouse: string]: number;
}

export interface Item extends Product {
  location: readonly [string, number];
  tags: readonly string[];
  stock: Stock;
}

export function totalStock(item: Item): number {
  let total = 0;
  for (const count of Object.values(item.stock)) total += count;
  return total;
}

export function stockValue(items: readonly Item[]): number {
  let value = 0;
  for (const item of items) value += item.price * totalStock(item);
  return value;
}

export const lamp: Item = {
  sku: "L-1",
  name: "Desk lamp",
  price: 25,
  location: ["A", 3],
  tags: ["light"],
  stock: { berlin: 4, bonn: 2 }
};

function noEdits(item: Item) {
  // @ts-expect-error: tags are read-only
  item.tags.push("sale");
  // @ts-expect-error: the stock is read-only
  item.stock.berlin = 0;
  // @ts-expect-error: a location cannot change
  item.location[1] = 4;
}
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 Product {
  readonly sku: string;
  name: string;
  price: number;
  note: string;
}

export interface Item extends Product {
  location: [string, number];
  tags: string[];
  stock: number;
}

export function totalStock(item: Item): number {
  return item.stock;
}

export function stockValue(items: readonly Item[]): number {
  let value = 0;
  for (const item of items) value += item.price * totalStock(item);
  return value;
}

export const lamp: Item = {
  sku: "L-1",
  name: "Desk lamp",
  price: 25,
  location: ["A", 3],
  tags: ["light"],
  stock: { berlin: 4, bonn: 2 }
};

function noEdits(item: Item) {
  // @ts-expect-error: tags are read-only
  item.tags.push("sale");
  // @ts-expect-error: the stock is read-only
  item.stock.berlin = 0;
  // @ts-expect-error: a location cannot change
  item.location[1] = 4;
}

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {totalStock, stockValue, lamp} from './main.ts';

const chair = {sku: 'C-7', name: 'Chair', price: 80, location: ['B', 1] as const, tags: [], stock: {berlin: 1}};
const empty = {sku: 'E-0', name: 'Empty box', price: 2, location: ['C', 2] as const, tags: [], stock: {}};

test('totalStock addiert die Bestände aller Lager', () => {
  assert.equal(totalStock(lamp), 6, `totalStock(lamp) ergab ${totalStock(lamp)}`);
});

test('ein Artikel ohne Bestandseinträge hat den Bestand 0', () => {
  assert.equal(totalStock(empty), 0, `totalStock(empty) ergab ${totalStock(empty)}`);
});

test('stockValue addiert Preis mal Bestand über alle Artikel', () => {
  const got = stockValue([lamp, chair, empty]);
  assert.equal(got, 230, `stockValue([lamp, chair, empty]) ergab ${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 3

Schritt 2: einen Artikel aus unknown prüfen

parseItem prüft sku, name und price, aber isLocation und isStock prüfen zu wenig und geben einen einfachen boolean zurück, also kann tsc location und stock nicht verengen, und tags wird gar nicht geprüft. Machen Sie beide zu Type Predicates. isLocation: genau zwei Elemente, ein String und eine Zahl. isStock: ein Record, dessen Werte alle Zahlen sind. Ergänzen Sie ein Predicate für tags, ein Array von Strings, und brechen Sie mit tags must be strings ab, wenn es nicht zutrifft.

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 Predicate ist ein Rückgabetyp: function isLocation(value: unknown): value is Location {

  2. Hinweis 2

    Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "number"

  3. Hinweis 3

    isRecord(value) && Object.values(value).every((count) => typeof count === "number") prüft jedes Lager.

Eine Lösung zeigen

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

export interface Product {
  readonly sku: string;
  name: string;
  price: number;
  note?: string;
}

export interface Stock {
  readonly [warehouse: string]: number;
}

export type Location = readonly [string, number];

export interface Item extends Product {
  location: Location;
  tags: readonly string[];
  stock: Stock;
}

function fail(message: string): never {
  throw new Error(message);
}

function isRecord(value: unknown): value is { [key: string]: unknown } {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

export function isLocation(value: unknown): value is Location {
  return Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "number";
}

export function isStock(value: unknown): value is Stock {
  return isRecord(value) && Object.values(value).every((count) => typeof count === "number");
}

function isStringArray(value: unknown): value is readonly string[] {
  return Array.isArray(value) && value.every((tag) => typeof tag === "string");
}

export function parseItem(value: unknown): Item {
  if (!isRecord(value)) fail("an item must be an object");
  const { sku, name, price, location, tags, stock } = value;
  if (typeof sku !== "string") fail("sku must be a string");
  if (typeof name !== "string") fail("name must be a string");
  if (typeof price !== "number") fail("price must be a number");
  if (!isLocation(location)) fail("location must be [aisle, shelf]");
  if (!isStringArray(tags)) fail("tags must be strings");
  if (!isStock(stock)) fail("stock must map warehouses to numbers");
  return { sku, name, price, location, tags, stock };
}
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 Product {
  readonly sku: string;
  name: string;
  price: number;
  note?: string;
}

export interface Stock {
  readonly [warehouse: string]: number;
}

export type Location = readonly [string, number];

export interface Item extends Product {
  location: Location;
  tags: readonly string[];
  stock: Stock;
}

function fail(message: string): never {
  throw new Error(message);
}

function isRecord(value: unknown): value is { [key: string]: unknown } {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

export function isLocation(value: unknown): boolean {
  return Array.isArray(value);
}

export function isStock(value: unknown): boolean {
  return isRecord(value);
}

export function parseItem(value: unknown): Item {
  if (!isRecord(value)) fail("an item must be an object");
  const { sku, name, price, location, tags, stock } = value;
  if (typeof sku !== "string") fail("sku must be a string");
  if (typeof name !== "string") fail("name must be a string");
  if (typeof price !== "number") fail("price must be a number");
  if (!isLocation(location)) fail("location must be [aisle, shelf]");
  if (!isStock(stock)) fail("stock must map warehouses to numbers");
  return { sku, name, price, location, tags, stock };
}

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {isLocation, isStock, parseItem} from './main.ts';

const lamp = {sku: 'L-1', name: 'Desk lamp', price: 25, location: ['A', 3], tags: ['light'], stock: {berlin: 4, bonn: 2}};

test('isLocation akzeptiert einen Gang und ein Regal, sonst nichts', () => {
  assert.equal(isLocation(['A', 3]), true, "isLocation(['A', 3]) sollte true sein");
  assert.equal(isLocation(['A', '3']), false, "isLocation(['A', '3']) sollte false sein: das Regal ist eine Zahl");
  assert.equal(isLocation(['A', 3, 1]), false, "isLocation(['A', 3, 1]) sollte false sein: genau zwei Teile");
});

test('isStock akzeptiert nur Zahlen pro Lager', () => {
  assert.equal(isStock({berlin: 4}), true, 'isStock({berlin: 4}) sollte true sein');
  assert.equal(isStock({berlin: 'four'}), false, "isStock({berlin: 'four'}) sollte false sein");
  assert.equal(isStock([4]), false, 'isStock([4]) sollte false sein: ein Array ist keine Zuordnung von Lagern');
});

test('ein gültiger Artikel kommt mit allen Eigenschaften zurück', () => {
  assert.deepEqual(parseItem(lamp), lamp, 'parseItem(lamp) sollte ein Objekt gleich lamp zurückgeben');
});

test('tags muss ein Array von Strings sein', () => {
  assert.throws(() => parseItem({...lamp, tags: ['light', 7]}), {message: 'tags must be strings'}, 'ein Tag 7 sollte "tags must be strings" werfen');
});

test('ein falscher Ort oder Bestand wird mit seiner Meldung abgelehnt', () => {
  assert.throws(() => parseItem({...lamp, location: ['A']}), {message: 'location must be [aisle, shelf]'}, "location ['A'] sollte \"location must be [aisle, shelf]\" werfen");
  assert.throws(() => parseItem({...lamp, stock: {berlin: '4'}}), {message: 'stock must map warehouses to numbers'}, "stock {berlin: '4'} sollte \"stock must map warehouses to numbers\" werfen");
});

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 3 von 3

Schritt 3: den Bestand laden und knappe Artikel auflisten

parseInventory vertraut JSON.parse, und lowStock sortiert die readonly-Artikel an Ort und Stelle, was tsc mit error TS2339 ablehnt. Lassen Sie parseInventory den Text als unknown lesen und prüfen: Brechen Sie mit the inventory must be an object, updated must be a string oder items must be an array ab, und prüfen Sie jeden Artikel mit parseItem. lowStock gibt die SKUs zurück, deren Gesamtbestand höchstens die Grenze erreicht, sortiert, ohne die Reihenfolge der Artikel zu ändern.

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

    const raw: unknown = JSON.parse(text); dann if (!isRecord(raw)) fail("the inventory must be an object");

  2. Hinweis 2

    Nach if (!Array.isArray(items)) fail(…) prüft items.map(parseItem) jeden Artikel und ergibt ein Item[].

  3. Hinweis 3

    toSorted gibt eine sortierte Kopie zurück und lässt inventory.items unverändert.

Eine Lösung zeigen

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

export interface Product {
  readonly sku: string;
  name: string;
  price: number;
  note?: string;
}

export interface Stock {
  readonly [warehouse: string]: number;
}

export type Location = readonly [string, number];

export interface Item extends Product {
  location: Location;
  tags: readonly string[];
  stock: Stock;
}

export interface Inventory {
  readonly updated: string;
  readonly items: readonly Item[];
}

function fail(message: string): never {
  throw new Error(message);
}

function isRecord(value: unknown): value is { [key: string]: unknown } {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function isLocation(value: unknown): value is Location {
  return Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "number";
}

function isStock(value: unknown): value is Stock {
  return isRecord(value) && Object.values(value).every((count) => typeof count === "number");
}

function isStringArray(value: unknown): value is readonly string[] {
  return Array.isArray(value) && value.every((tag) => typeof tag === "string");
}

export function parseItem(value: unknown): Item {
  if (!isRecord(value)) fail("an item must be an object");
  const { sku, name, price, location, tags, stock } = value;
  if (typeof sku !== "string") fail("sku must be a string");
  if (typeof name !== "string") fail("name must be a string");
  if (typeof price !== "number") fail("price must be a number");
  if (!isLocation(location)) fail("location must be [aisle, shelf]");
  if (!isStringArray(tags)) fail("tags must be strings");
  if (!isStock(stock)) fail("stock must map warehouses to numbers");
  return { sku, name, price, location, tags, stock };
}

export function totalStock(item: Item): number {
  let total = 0;
  for (const count of Object.values(item.stock)) total += count;
  return total;
}

export function parseInventory(text: string): Inventory {
  const raw: unknown = JSON.parse(text);
  if (!isRecord(raw)) fail("the inventory must be an object");
  const { updated, items } = raw;
  if (typeof updated !== "string") fail("updated must be a string");
  if (!Array.isArray(items)) fail("items must be an array");
  return { updated, items: items.map(parseItem) };
}

export function lowStock(inventory: Inventory, limit: number): readonly string[] {
  const sorted = inventory.items.toSorted((a, b) => a.sku.localeCompare(b.sku));
  return sorted.filter((item) => totalStock(item) <= limit).map((item) => item.sku);
}
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 Product {
  readonly sku: string;
  name: string;
  price: number;
  note?: string;
}

export interface Stock {
  readonly [warehouse: string]: number;
}

export type Location = readonly [string, number];

export interface Item extends Product {
  location: Location;
  tags: readonly string[];
  stock: Stock;
}

export interface Inventory {
  readonly updated: string;
  readonly items: readonly Item[];
}

function fail(message: string): never {
  throw new Error(message);
}

function isRecord(value: unknown): value is { [key: string]: unknown } {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function isLocation(value: unknown): value is Location {
  return Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "number";
}

function isStock(value: unknown): value is Stock {
  return isRecord(value) && Object.values(value).every((count) => typeof count === "number");
}

function isStringArray(value: unknown): value is readonly string[] {
  return Array.isArray(value) && value.every((tag) => typeof tag === "string");
}

export function parseItem(value: unknown): Item {
  if (!isRecord(value)) fail("an item must be an object");
  const { sku, name, price, location, tags, stock } = value;
  if (typeof sku !== "string") fail("sku must be a string");
  if (typeof name !== "string") fail("name must be a string");
  if (typeof price !== "number") fail("price must be a number");
  if (!isLocation(location)) fail("location must be [aisle, shelf]");
  if (!isStringArray(tags)) fail("tags must be strings");
  if (!isStock(stock)) fail("stock must map warehouses to numbers");
  return { sku, name, price, location, tags, stock };
}

export function totalStock(item: Item): number {
  let total = 0;
  for (const count of Object.values(item.stock)) total += count;
  return total;
}

export function parseInventory(text: string): Inventory {
  const raw = JSON.parse(text);
  return { updated: raw.updated, items: raw.items };
}

export function lowStock(inventory: Inventory, limit: number): readonly string[] {
  const sorted = inventory.items.sort((a, b) => a.sku.localeCompare(b.sku));
  return sorted.filter((item) => totalStock(item) <= limit).map((item) => item.sku);
}

main.test.ts

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

const item = (sku, berlin) => ({sku, name: sku, price: 1, location: ['A', 1], tags: [], stock: {berlin}});
const text = JSON.stringify({updated: '2026-10-04', items: [item('L-1', 6), item('C-7', 1), item('B-2', 3)]});

test('ein gültiger Bestand wird mit allen Artikeln geladen', () => {
  const inventory = parseInventory(text);
  assert.equal(inventory.updated, '2026-10-04', `updated ist ${JSON.stringify(inventory.updated)}`);
  assert.deepEqual(inventory.items.map((i) => i.sku), ['L-1', 'C-7', 'B-2'], 'die drei Artikel sollten in ihrer Reihenfolge geladen werden');
});

test('der Bestand selbst wird geprüft', () => {
  assert.throws(() => parseInventory('null'), {message: 'the inventory must be an object'}, 'null sollte "the inventory must be an object" werfen');
  assert.throws(() => parseInventory('{"updated": "2026-10-04", "items": 3}'), {message: 'items must be an array'}, 'items: 3 sollte "items must be an array" werfen');
});

test('auch jeder Artikel wird geprüft', () => {
  const bad = JSON.stringify({updated: '2026-10-04', items: [{...item('X-1', 1), price: 'cheap'}]});
  assert.throws(() => parseInventory(bad), {message: 'price must be a number'}, 'ein Artikel mit price "cheap" sollte "price must be a number" werfen');
});

test('lowStock listet die SKUs bis zur Grenze, sortiert', () => {
  const got = lowStock(parseInventory(text), 3);
  assert.deepEqual(got, ['B-2', 'C-7'], `lowStock(inventory, 3) ergab ${JSON.stringify(got)}`);
});

test('lowStock lässt die Reihenfolge der Artikel unverändert', () => {
  const inventory = parseInventory(text);
  lowStock(inventory, 3);
  assert.deepEqual(inventory.items.map((i) => i.sku), ['L-1', 'C-7', 'B-2'], 'nach lowStock sollten die Artikel noch in ihrer Reihenfolge sein');
});

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

JSON.parse über eine Annotation vertrauen

interface Inventory {
  items: { sku: string }[];
}

const inventory: Inventory = JSON.parse('{ "products": [] }');
console.log(inventory.items.length);

Was tsc oder Node.js ausgibt

TypeError: Cannot read properties of undefined (reading 'length')

Warum, und die Lösung

JSON.parse gibt any zurück, also akzeptiert tsc die Annotation, ohne den Text anzusehen, und der Fehler zeigt sich erst beim Ausführen. Hier enthält der Text products, nicht items. Lesen Sie das Ergebnis als unknown und prüfen Sie es, bevor Sie daraus ein Inventory bauen, wie parseInventory es tut; dann stoppt eine falsche Datei mit einer klaren Meldung wie items must be an array.

Eine Eigenschaft von unknown lesen

function skuOf(value: unknown): string {
  return value.sku;
}

Was tsc oder Node.js ausgibt

main.ts(2,10): error TS18046: 'value' is of type 'unknown'.

Warum, und die Lösung

unknown erlaubt nichts, bis Sie geprüft haben, was es ist; dafür ist es da. Prüfen Sie zuerst mit einem Predicate wie isRecord, dass es ein Objekt ist, dann die Eigenschaft selbst: if (!isRecord(value)) fail("not an object"); const { sku } = value; if (typeof sku !== "string") fail("sku must be a string"); return sku;

Etwas an eine readonly-Liste von Artikeln anhängen

interface Item {
  sku: string;
}
interface Inventory {
  readonly items: readonly Item[];
}

function addItem(inventory: Inventory, item: Item): void {
  inventory.items.push(item);
}

Was tsc oder Node.js ausgibt

main.ts(9,19): error TS2339: Property 'push' does not exist on type 'readonly Item[]'.

Warum, und die Lösung

Der geladene Bestand ist absichtlich readonly: Auswertungen dürfen ihn nicht ändern. Um einen Artikel hinzuzufügen, bauen Sie stattdessen einen neuen Bestand: function addItem(inventory: Inventory, item: Item): Inventory { return { ...inventory, items: [...inventory.items, item] }; }. Der alte bleibt, wie er war, und damit bleiben auch frühere Auswertungen gültig.

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

Ein Modell, alle Werkzeuge des Moduls

Jeder Teil der Daten sagt, was er ist. sku ist readonly, weil sie den Artikel identifiziert; note?: string darf fehlen. Der Bestand pro Lager hat Namen, die niemand im Voraus kennt: eine Indexsignatur, readonly [warehouse: string]: number. Item extends Product, also akzeptiert eine Funktion, die nur ein Product braucht, auch ein Item. tags ist ein readonly string[], location ein Tupel readonly [string, number]: erst der Gang, dann das Regal. Inventory enthält readonly items: readonly Item[]. So aufgeschrieben prüft tsc jede Funktion, die die Daten liest, und meldet jede, die ändern will, was fest bleiben soll.

Von unknown zum Modell: erst prüfen, dann vertrauen

JSON.parse gibt any zurück, also wird alles, was im Text steht, ungeprüft geglaubt. Annotieren Sie das Ergebnis stattdessen als unknown: Dann erlaubt tsc nichts, bis Sie prüfen (error TS18046). isRecord(value): value is { [key: string]: unknown } prüft auf ein Objekt, und typeof null ist "object", also testet es auch value !== null. Zerlegen Sie die Felder, prüfen Sie jedes mit typeof und brechen Sie mit fail(message): never ab. Schreiben Sie Type Predicates für die Teile: isLocation für das Tupel, isStock für die Zuordnung. Array.isArray verengt unknown zu any[], also muss ein Predicate auch die Elemente prüfen. tsc glaubt einem Predicate vollständig: Prüft es zu wenig, lügt der Typ.

Auswertungen lesen, sie schreiben nie

Einmal geladen, bleiben die Daten, wie sie sind. totalStock addiert Object.values(item.stock), was bei einer Indexsignatur von Zahlen ein number[] ist. lowStock sortiert mit toSorted eine Kopie, weil items readonly ist und kein sort hat; es gibt ein readonly string[] zurück, also kann auch der Aufrufer das Ergebnis nicht ändern. Testen Sie die Exporte mit node:test: assert.equal für eine Summe, assert.deepEqual für eine Liste und assert.throws(() => parseInventory(text), { message: "items must be an array" }) für eine abgelehnte Eingabe. Ein Test, der eine Auswertung ausführt und danach die Reihenfolge der Artikel vergleicht, beweist, dass nichts verändert wurde.

Quellen

Zuletzt geprüft am 4. Oktober 2026