Fortgeschritten6 Min. Lesezeit

TypeScript-Types aus DynamoDB generieren

In Postgres introspektierst du information_schema und generierst Types daraus. DynamoDB hat kein Äquivalent: DynamoDB speichert überhaupt kein Item-Schema. Die einzigen Attribute, die der Service kennt, sind die in Keys. DescribeTables AttributeDefinitions ist explizit über den eigenen Scope: jeder Eintrag „describes one attribute in the table and index key schema“ (AWS API reference) — deine anderen fünfzig Attribute sind nirgends erfasst.

„TypeScript-Types aus DynamoDB generieren“ heißt also immer eines von dreien: die Shape deklarieren, sie aus einem Schema ableiten, das du in Code authorst, oder sie aus den Items inferieren, die tatsächlich existieren.

Wie bekomme ich TypeScript-Types für eine DynamoDB-Tabelle?

Es gibt keine API, die die Item-Shape einer Tabelle zurückgibt — DescribeTable kennt nur die Key-Attribute. Deine Optionen: ein interface handschreiben und an der Boundary validieren (ein Zod-Schema macht Types und Runtime-Check zu einem Artefakt), eine schema-first Library nutzen, bei der das Schema, das du authorst, die Types liefert, oder die Shape aus echten Items inferieren — per Script oder mit einem Tool wie DynoTable, das die Tabelle scannt und ein TypeScript-Interface, Zod-Schema oder JSON Schema exportiert.

Methode 1: Interface handschreiben + an der Boundary validieren

Das AWS SDK kann deine Items nicht für dich typen. Der v3 Document Client liefert Items als untyped Records — jedes GetCommand/QueryCommand-Result ist effektiv Record<string, unknown>, bis du etwas anderes assertest. Ein nacktes as Order-Cast kompiliert und lügt zur Runtime — deshalb paart eine strikte Version das Interface mit einem Runtime-Check:

import {z} from 'zod';

const Order = z.object({
  PK: z.string(), // ORDER#<id>
  SK: z.string(), // META
  status: z.enum(['open', 'shipped', 'cancelled']),
  total: z.number(),
  couponCode: z.string().optional() // sparse attribute
});
type Order = z.infer<typeof Order>;

const {Item} = await doc.send(new GetCommand({TableName: 'Orders', Key: key}));
const order = Order.parse(Item); // typed AND verified

Ein Schema, zwei Jobs: z.infer gibt den Static Type, parse fängt das Item, das nicht passt — in einem schemalosen Store ein wann, kein ob. Der Haken ist ebenso klar: das Schema dokumentiert deine Absicht, nicht deine Tabelle. Nichts hindert einen alten Writer, total als String gespeichert zu haben, und handgeschriebene Types driften still, während sich die Daten entwickeln.

Arbeitest du mit Raw-(Non-Document-Client-)API-Output, denk an die Wire-Shape: type-tagged DynamoDB-JSON ({"S": "..."}, {"N": "123"}) — siehe Marshalling, und nutze den DynamoDB JSON Converter, um ein Sample zwischen Wire und Plain zu flippen, während du das Schema schreibst.

Methode 2: Schema-first Libraries

Toolkits wie ElectroDB und DynamoDB-Toolbox greifen das Drift-Problem von der Write-Seite an: du authorst ein Entity-Schema in Code, und die Library leitet die TypeScript-Types ab und erzwingt die Shape bei jedem Read und Write, den sie ausführt. Das ist die stärkste verfügbare Garantie — aber beachte die Richtung: du schreibst das Schema; die Library entdeckt es nicht. Eine bestehende Tabelle anzubinden heißt trotzdem, die Item-Shapes selbst reverse-engineeren, und Items außerhalb der Library liegen außerhalb ihrer Garantien. Sie glänzen bei Greenfield-Single-Table Designs, wo jede Entity von Tag eins durch das Toolkit geht.

Methode 3: Types aus echten Items inferieren

Bei einer bestehenden Tabelle ist die Ground Truth die Daten. Sample scannen, Shapes unionen:

const seen = new Map<string, Set<string>>(); // attr -> observed types
let count = 0;
let key: Record<string, unknown> | undefined;
do {
  const page = await doc.send(new ScanCommand({TableName: 'Orders', ExclusiveStartKey: key}));
  for (const item of page.Items ?? []) {
    count++;
    for (const [attr, value] of Object.entries(item)) {
      const t = Array.isArray(value) ? 'array' : typeof value;
      (seen.get(attr) ?? seen.set(attr, new Set()).get(attr)!).add(t);
    }
  }
  key = page.LastEvaluatedKey;
} while (key && count < 5000);
// emit: attribute -> type union, optional if seen in < count items

Fallstricke, die die naive Version sofort trifft:

  • Sparse Attributes. DynamoDB-Items in einer Tabelle können unterschiedliche Attribute haben; ein Attribut auf 80 % der Items ist optional, nicht fehlend. Per-Attribute-Frequency tracken, nicht nur Presence.
  • Gemischte Entities. In einem Single-Table Design teilen sich USER#- und ORDER#-Items die Tabelle — ein gemergtes Interface für beide ist nutzlos. Sample am Type Attribute partitionieren und einen Type pro Entity emittieren.
  • Type Collisions. Dasselbe Attribut hier als N, dort als S ist ein echter (und häufiger) Data Bug — als Union surfacer, nicht still eines wählen. Das volle Tag-Set steht in Data Types.
  • Ein Sample ist ein Sample. Attribute, die nur auf seltenen Items liegen, fehlen vielleicht in den ersten 5.000 — und der Scan kostet trotzdem Read Capacity (Query vs Scan).

One-Click-Inference in DynoTable

Dieses Inference-Script — Sampling, Frequency-Tracking, Nested Paths, Per-Entity- Split — steckt in DynoTables Tabellen-Dialog:

  1. Tabelle öffnen, Einstellungen-Button in der Tab-Toolbar, Bereich Indexierung, dann Tabelle indexieren. DynoTable sammelt die Tabelle mit Live-Progress und erfasst die gefundenen Attribute — inklusive nested per Dotted Path wie commonData.status — mit Type und ob required oder optional über die gescannten Rows. Der Scan ist capped, ein Attribut nur auf seltenen Items kann fehlen; siehe Tabellenübersicht und -indexierung.
  2. Exportieren und Format wählen:
    • TypeScript — ein interface.
    • Zod — ein z.object(...)-Schema (Standard-Schema-kompatibel).
    • JSON Schema — Draft 2020-12.
  3. In die Zwischenablage kopieren oder als Datei speichern.
DynoTables Bereich „Indexierung“: die indexierte Feldliste mit Types und Required/Optional-Flags sowie dem Schema-Export-Button.
DynoTables Bereich „Indexierung“: die indexierte Feldliste mit Types und Required/Optional-Flags sowie dem Schema-Export-Button.

Jedes generierte Schema beginnt mit dem Hinweis, dass es aus den gesampelten Items inferiert wurde — ein starker Startpunkt, kein autoritativer Vertrag. Optionalität spiegelt, wie oft jedes Attribut beim Indexing vorkam; Primary-Key- Attribute sind immer required. Indexing verursacht normale DynamoDB-Read-Kosten, und Neu indexieren aktualisiert das Bild, nachdem sich deine Daten geändert haben.

FAQ

Kann ich Types aus DescribeTable generieren? Nur für die Key-Attribute. AttributeDefinitions deckt Table- und Index-Key- Schema ab — sonst nichts über deine Items ist vom Service gespeichert, also gibt es kein Server-Side-Schema zum Introspektieren.

Was ist der beste Weg, eine bestehende Production-Tabelle zu typen? Zuerst inferieren, dann härten: echte Items sampeln (Script oder DynoTables indexierter Export) für die tatsächliche Shape, reviewen und zu einem hand-owned Zod-Schema oder einer schema-first Library-Entity promoten, damit zukünftiger Drift an der Boundary auffällt.

Wie gehe ich mit mehreren Entity-Types in einer Tabelle um? Ein Type pro Entity, nie ein gemergter Type. Sample am Type Attribute (oder Key-Prefix) splitten und ein separates Interface pro Entity generieren — die discriminated Union davon ist dein Table Type.

Warum sagen meine generierten Types, ein required Field sei optional? Weil irgendein gesampeltes Item es nicht hatte. In einem schemalosen Store ist Optionalität eine Observation, keine Deklaration — prüfe, ob diese Items Legacy- Rows zum Backfill sind (siehe Migrations) oder ein echt optionales Attribut.

Decken die Types DynamoDB-Sets und Binary ab? Ein Converter muss Plain-JSON-Repräsentationen wählen: Sets werden Arrays, Binary ein encoded String — dieselben Mapping-Quirks wie in Marshalling. Ein Sample durch den DynamoDB JSON Converter round-trippen, um zu sehen, wie deine Attribute auf jeder Seite aussehen.

Hör auf, die Shape deiner Tabelle zu raten — DynoTable herunterladen, Tabelle indexieren und TypeScript, Zod oder JSON Schema mit einem Klick exportieren.

Aktualisiert