Fortgeschritten6 Min. Lesezeit

Wie man TypeScript-Typen aus einer DynamoDB-Tabelle generiert

In Postgres würdest du information_schema introspizieren und daraus Typen generieren. DynamoDB hat kein Äquivalent — und das ist kein fehlendes Feature, es ist das Datenmodell: DynamoDB speichert überhaupt kein Item-Schema. Die einzigen Attribute, die der Service kennt, sind die in Schlüsseln verwendeten. DescribeTables AttributeDefinitions ist explizit, was den eigenen Geltungsbereich angeht: Jeder Eintrag „beschreibt ein Attribut im Schlüsselschema der Tabelle und der Indizes" (AWS-API-Referenz) — die anderen fünfzig Attribute deiner Tabelle sind schlicht nirgendwo verzeichnet.

„TypeScript-Typen aus DynamoDB generieren" heißt also immer eines von drei Dingen: die Form selbst deklarieren, sie aus einem in Code verfassten Schema ableiten oder sie aus den tatsächlich existierenden Items inferieren.

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

Es gibt keine API, die die Item-Form einer Tabelle zurückgibt — DescribeTable kennt nur die Schlüsselattribute. Deine Optionen: ein interface von Hand schreiben und an der Grenze validieren (ein Zod-Schema macht Typen und Laufzeit-Check zu einem Artefakt), eine Schema-first-Bibliothek nutzen, bei der das von dir verfasste Schema die Typen liefert, oder die Form aus echten Items inferieren — per Skript oder mit einem Tool wie DynoTable, das die Tabelle scannt und ein TypeScript-Interface, Zod-Schema oder JSON Schema exportiert.

Methode 1: das Interface von Hand schreiben + an der Grenze validieren

Das AWS SDK kann deine Items nicht für dich typisieren. Der v3 Document Client liefert Items als untypisierte Records — jedes GetCommand-/ QueryCommand-Ergebnis ist faktisch Record<string, unknown>, bis du etwas anderes behauptest. Ein nacktes as Order kompiliert problemlos und lügt zur Laufzeit, weshalb die robuste Variante das Interface mit einem Laufzeit-Check paart:

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 dir den statischen Typ, parse fängt das Item, das nicht dazu passt — was in einem schemalosen Store ein Wann ist, kein Ob. Der Haken ist genauso schlicht: Das Schema dokumentiert deine Absicht, nicht deine Tabelle. Nichts hindert einen alten Writer daran, total als String gespeichert zu haben, und handgeschriebene Typen driften stillschweigend, während die Daten sich weiterentwickeln.

Arbeitest du mit roher (Nicht-Document-Client-)API-Ausgabe, denk daran, dass das Wire-Format typ-getaggtes DynamoDB-JSON ist ({"S": "..."}, {"N": "123"}) — siehe Marshalling, und nutze den DynamoDB-JSON-Konverter, um eine Probe zwischen Wire- und Klartext-Form hin- und herzuschalten, während du das Schema schreibst.

Methode 2: Schema-first-Bibliotheken

Toolkits wie ElectroDB und DynamoDB-Toolbox greifen das Drift-Problem von der Schreibseite an: Du verfasst ein Entity-Schema in Code, und die Bibliothek leitet die TypeScript-Typen ab und erzwingt die Form bei jedem Lese- und Schreibvorgang, den sie ausführt. Das ist die stärkste verfügbare Garantie — aber beachte die Richtung: Du schreibst das Schema; die Bibliothek entdeckt es nicht. Eine davon auf eine bestehende Tabelle zu richten heißt weiterhin, die Item-Formen zuerst selbst zurückzuentwickeln, und Items, die außerhalb der Bibliothek geschrieben wurden, liegen außerhalb ihrer Garantien. Sie glänzen bei Greenfield-Single-Table-Designs, in denen jede Entity vom ersten Tag an durch das Toolkit läuft.

Methode 3: die Typen aus echten Items inferieren

Für eine bestehende Tabelle sind die Daten die Wahrheit. Scanne eine Stichprobe, vereinige die Formen:

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

Praxisfallen, in die die naive Version sofort läuft:

  • Sparse-Attribute. DynamoDB-Items in einer Tabelle können unterschiedliche Attribute haben; ein Attribut, das auf 80 % der Items vorhanden ist, ist optional, nicht fehlend. Verfolge die Häufigkeit pro Attribut, nicht nur die Präsenz.
  • Gemischte Entities. In einem Single-Table-Design teilen sich USER#- und ORDER#-Items die Tabelle — ein zusammengelegtes Interface für beide ist nutzlos. Partitioniere die Stichprobe nach dem Typ-Attribut und emittiere einen Typ pro Entity.
  • Typkollisionen. Dasselbe Attribut hier als N und dort als S gespeichert ist ein echter (und häufiger) Datenfehler — mache ihn als Union sichtbar, statt stillschweigend einen Typ zu wählen. Der vollständige Tag-Satz steht in Datentypen.
  • Eine Stichprobe ist eine Stichprobe. Attribute, die nur auf seltenen Items vorkommen, sind vielleicht nicht in deinen ersten 5.000 — und der Scan kostet so oder so Lesekapazität (Query vs Scan).

Ein-Klick-Inferenz in DynoTable

Dieses Inferenz-Skript — Sampling, Häufigkeits-Tracking, verschachtelte Pfade, die Aufteilung pro Entity — ist in das Table-stats-Panel von DynoTable eingebaut:

  1. Öffne eine Tabelle, drücke den Stats-Button (das Balkendiagramm-Icon) und klicke Index table. DynoTable scannt die Tabelle einmal mit Live-Fortschritt und entdeckt jedes Attribut, das deine Items tragen — einschließlich verschachtelter über Punktpfade wie commonData.status — mit seinem Typ und ob es über die gescannten Zeilen hinweg required oder optional war.
  2. Klicke Export und wähle ein Format:
    • TypeScript — ein interface.
    • Zod — ein z.object(...)-Schema (Standard-Schema-kompatibel).
    • JSON Schema — Draft 2020-12.
  3. Kopiere es in die Zwischenablage oder speichere es in eine Datei.
DynoTables Table-stats-Panel: die Liste der indizierten Felder mit Typen und Required-/Optional-Flags und der Schema-Export-Button.
DynoTables Table-stats-Panel: die Liste der indizierten Felder mit Typen und Required-/Optional-Flags und der Schema-Export-Button.

Der Export ist ehrlich, was er ist: Jedes generierte Schema beginnt mit einem Hinweis, dass es aus den gesampelten Items inferiert wurde — ein starker Ausgangspunkt, kein autoritativer Vertrag. Die Optionalität spiegelt wider, wie oft jedes Attribut beim Indizieren auftauchte, und Primary-Key-Attribute sind immer als required markiert. Das Indizieren verursacht normale DynamoDB-Lesekosten, und Reindex frischt das Bild nach Datenänderungen auf.

FAQ

Kann ich Typen aus DescribeTable generieren? Nur für die Schlüsselattribute. AttributeDefinitions deckt das Schlüsselschema von Tabelle und Indizes ab — sonst speichert der Service nichts über deine Items, es gibt also kein serverseitiges Schema zum Introspizieren.

Was ist der beste Weg, eine bestehende Produktionstabelle zu typisieren? Erst inferieren, dann härten: Sample die echten Items (per Skript oder DynoTables indiziertem Export), um die tatsächliche Form zu bekommen, prüfe sie und befördere sie zu einem selbst gepflegten Zod-Schema oder einer Schema-first-Bibliotheks-Entity, damit künftige Drift an der Grenze auffällt.

Wie gehe ich mit mehreren Entity-Typen in einer Tabelle um? Ein Typ pro Entity, nie ein zusammengelegter Typ. Teile die Stichprobe an deinem Typ-Attribut (oder Schlüsselpräfix) und generiere ein separates Interface für jede — die Discriminated Union daraus ist dein Tabellentyp.

Warum sagen meine generierten Typen, ein Pflichtfeld sei optional? Weil irgendein gesampeltes Item es nicht hatte. In einem schemalosen Store ist Optionalität eine Beobachtung, keine Deklaration — prüfe, ob diese Items Altzeilen zum Nachfüllen sind (siehe Migrationen) oder ein wirklich optionales Attribut.

Decken die Typen DynamoDB-Sets und Binary ab? Ein Konverter muss Klartext-JSON-Darstellungen wählen: Sets werden zu Arrays und Binary zu einem enkodierten String — dieselben Mapping-Eigenheiten wie in Marshalling. Schicke eine Probe im Round-Trip durch den DynamoDB-JSON-Konverter, um exakt zu sehen, wie deine Attribute auf jeder Seite aussehen.

Hör auf, die Form deiner Tabelle zu raten — lade DynoTable herunter, indiziere die Tabelle und exportiere ein TypeScript-, Zod- oder JSON-Schema mit einem Klick.

Aktualisiert