JSON Toolkit

JSON nach TypeScript: Interfaces, Type Guards und die Grenzen der Generierung

Fazit: Aus Beispiel-JSON ein Interface zu generieren ist die halbe Arbeit: optionale Felder, null, Unions, Daten und große Ganzzahlen brauchen einen menschlichen Durchgang, und Typen validieren zur Laufzeit nichts.

Fortgeschritten~9 Min. LesezeitVeröffentlicht 2026-10-31Aktualisiert 2026-11-04
Titelbild für den Leitfaden JSON zu TypeScript-Interface und Type Guards

Kurz gesagt: Generieren Sie das Interface, um Tipparbeit zu sparen, prüfen Sie dann Optionalität und null nach, halten Sie Daten und große Ganzzahlen als Strings und ergänzen Sie Laufzeitvalidierung an der Grenze; generierte Typen als Vertrag zu behandeln gibt Ihnen gar keine Validierung.

Ein Generator sieht nur das eine Beispiel, das Sie hineinkopieren. Ob Felder fehlen können, ob Werte null sein können und welches Format Daten haben, sind alles Vermutungen – und falsche Vermutungen müssen Sie korrigieren.

1. Was der Generator tatsächlich tut

Fügen Sie ein JSON-Beispiel in JSON to TypeScript ein, und das Interface, das Sie zurückbekommen, ist eine Formzusammenfassung: Es zählt die gesehenen Schlüssel und Werttypen und wählt dann die engste verfügbare Darstellung.

{
  "id": 1,
  "email": "ada@example.com",
  "profile": { "nickname": "Ada", "age": 36 },
  "tags": ["admin", "beta"],
  "createdAt": "2026-10-31T08:00:00Z"
}

Die Ausgabe sieht ungefähr so aus:

interface User {
  id: number;
  email: string;
  profile: { nickname: string; age: number };
  tags: string[];
  createdAt: string;
}

Das kompiliert, beschreibt aber einen Idealwert, nicht den echten Vertrag des Endpunkts. Der Generator weiß nicht, ob email fehlen kann, ob tags leer sein kann oder ob createdAt immer eine Zeitzone trägt. Er weiß nicht einmal, dass id in anderen Datensätzen ein String sein könnte.

Die richtige Erwartung ist also: Der Generator spart Tipparbeit, der Mensch liefert die Bedeutung.

2. Optionale Felder und null: ? ist nicht | null

Das ist der Teil der generierten Ausgabe, der am häufigsten kopiert und am häufigsten kaputt gemacht wird.

TypeScript-Form Bedeutung Passendes JSON
email: string Immer vorhanden, immer ein String {"email":"a@b.c"}
email?: string Schlüssel kann fehlen; wenn vorhanden, darf er nicht null sein {} oder {"email":"a@b.c"}
email: string | null Schlüssel ist immer vorhanden, Wert darf null sein {"email":null}
email?: string | null Schlüssel kann fehlen und der Wert darf null sein Alle drei Fälle

Der Generator hat ein Beispiel gesehen, gibt also meist die erste Form aus. Kann die API tatsächlich null liefern, lügt der Typ: alles kompiliert, und dann wirft email.toLowerCase() zur Laufzeit.

Es gibt zwei Wege zur Korrektur:

  1. Mehrere echte Beispiele sammeln, die sowohl „Schlüssel fehlt“ als auch „Wert ist null“ abdecken, und den Generator erneut inferieren lassen.
  2. Nach dem Generieren Felder von Hand zu ?: T | null ändern und beide Zweige im Code tatsächlich behandeln.

Nehmen Sie nicht die Abkürzung, alles als ?: string zu markieren. Optional bedeutet „der Schlüssel existiert vielleicht nicht“, nicht „der Wert darf leer sein“. undefined und null verhalten sich auch in JSON.stringify unterschiedlich: Ersteres wird weggelassen, Letzteres als null behalten.

3. Uneinheitliche Array-Elemente und Union-Typen

Arrays sind die am leichtesten falsch erfasste Struktur. Erscheint tags über drei Datensätze als ["admin"], [] und ["beta", 3], gibt der Generator je nach gesehenen Beispielen string[], (string | number)[] oder unknown[] aus.

Ein Mensch muss entscheiden:

  • Die Elementtypen sind tatsächlich gemischt: (string | number)[] schreiben und an der Verwendungsstelle einschränken.
  • Nur ein Datensatz ist schmutzig: Die Daten korrigieren. Nicht mit any[] verstecken.
  • Leere Arrays: Der Generator kann keinen Elementtyp inferieren und fällt auf never[] oder any[] zurück, Sie müssen ihn ergänzen.
  • Tupel fester Länge: [string, string] nur verwenden, wenn die Fachlogik die Länge wirklich garantiert; sonst wird ein später hinzugefügtes Element zum Kompilierfehler.

Objekt-Arrays brauchen bei uneinheitlichen Schlüsseln ebenfalls Aufmerksamkeit:

interface OrderItem {
  sku: string;
  quantity: number;
  discount?: number; // some records do not carry this field
}

Legen Sie die Optionalität auf den Elementtyp, nicht auf das Array. items?: OrderItem[] bedeutet, das Array selbst kann fehlen – eine völlig andere Aussage.

4. Daten, große Ganzzahlen, Enums: Typen, die JSON nicht hat

JSON hat nur Strings und Zahlen; die fachliche Bedeutung lebt in Konventionen. Der Generator inferiert aus Literalen, korrigieren Sie also von Hand:

JSON-Literal Übliche generierte Ausgabe Was es sein sollte
"2026-10-31T08:00:00Z" string In der Transportschicht string behalten und an der Verwendungsstelle konvertieren; niemals direkt Date deklarieren
1717075200000 number Für einen Millisekunden-Timestamp number verwenden und die Einheit dokumentieren
"1234567890123456789" string Als string behalten, damit number keine Präzision verlieren kann
1 und "1" gemischt number | string Auf eine Form normalisieren; die Union nicht in den Geschäftscode schieben
"admin" string Eine Literal-Union 'admin' | 'editor' oder eine geteilte Enum-Konstante
true und "true" gemischt boolean | string Die Datenquelle korrigieren, statt diesen Typ zu generieren

Die wichtigste Regel: Daten in JSON sind immer Strings. createdAt: Date zu schreiben ist eine verbreitete Selbsttäuschung – JSON.parse konstruiert nie ein Date, der Laufzeitwert ist also weiterhin ein String, und .getTime() wirft. Behalten Sie den Transporttyp als string, rufen Sie an der Grenze ausdrücklich new Date(value) auf und prüfen Sie Number.isNaN.

Große Ganzzahlen funktionieren genauso. Sobald eine id 2^53 übersteigt, hat JSON.parse bereits Präzision verloren, und number zu generieren zementiert den Bug nur in den Typ. Das Backend liefert einen String, das Frontend behält ihn als String; das ist die einzige verlässliche Option.

5. Generierte Typen validieren zur Laufzeit nichts

TypeScript-Typen werden zur Kompilierzeit gelöscht. So präzise das Interface auch ist, es kann eine Antwort {"id": "x"} nicht aufhalten – es beschränkt Ihren eigenen Code, nicht die Bytes, die über das Netz ankommen.

/**
 * Check whether a runtime value is a user object.
 * @param value The runtime value to validate
 * @returns Whether it satisfies the minimum shape of User
 */
function isUser(value: unknown): value is User {
  if (typeof value !== 'object' || value === null) {
    return false;
  }
  const candidate = value as Record<string, unknown>;
  return typeof candidate.id === 'number' && typeof candidate.email === 'string';
}

Ein Type Guard deckt nur die Felder ab, die Sie in die Prüfung schreiben. Mit mehr Feldern werden handgeschriebene Guards zu einer zweiten Definition, die gepflegt werden muss und vom Interface abdriftet.

Die praktische Abwägung sieht so aus:

Ansatz Laufzeitvalidierung Quelle der Typen Passt zu
Nur Interface Keine Handgeschrieben oder generiert Vertrauenswürdige interne Daten, Beschränkungen zur Kompilierzeit
Interface plus handgeschriebener Guard Ja Zwei Definitionen, die driften Wenige Felder, klare Grenze
zod / valibot Ja Typen aus dem Schema inferiert Externe APIs, Formulare, Konfiguration
JSON Schema plus Validator Ja Typen aus dem Schema generiert Sprachübergreifende Verträge, Dokumentation

Kommen die Daten von außen, machen Sie das Schema zur einzigen Wahrheitsquelle und leiten Sie Typen mit z.infer oder json-schema-to-ts ab. Dann sind „Validierung besteht“ und „der Typ ist korrekt“ dieselbe Tatsache, und es gibt keine zweite Definition, die driften kann. Zum vollständigen Zweiweg-Synchronisierungsansatz siehe Synchronisierung von Schema und TypeScript-Typen.

6. Interface oder type: nicht über generierte Ausgabe streiten

Generatoren geben meist interface aus, aber beide sind in den meisten Situationen austauschbar. Was tatsächlich zählt, sind drei Fähigkeiten:

  • Declaration Merging: interface kann mehrfach deklariert werden und verschmilzt automatisch; type kann das nicht. Das hilft beim Patchen von Drittanbieter-Typen, Geschäftsmodelle sollten es aber vermeiden – Merging lässt „zusätzliche Felder“ unbemerkt durch den Compiler schlüpfen.
  • Unions und Mapped Types: type kann A | B, Partial<T> und Template-Literal-Typen direkt ausdrücken; interface nicht.
  • Rekursion: Beide unterstützen sie, aber komplexe rekursive Formen lesen sich als type natürlicher.

Eine nützlichere Teamregel: interface für Objektformen, type für Unions, Utility-Typen und Funktionstypen. Lassen Sie generierte Ausgabe auf ihrem Standard und verwenden Sie die Review-Zeit stattdessen auf Optionalität und Laufzeitvalidierung.

7. Toolchain: drei Wege zu generieren

Ansatz Eingabe Stärke Einschränkung
JSON to TypeScript dieser Website Beispiel-JSON Sofort nutzbar, lokal verarbeitet Nur auf dem aktuellen Beispiel basierend; manuelle Einschränkung nötig
quicktype JSON / JSON Schema Mehrsprachige Ausgabe, CLI-Integration Konservative Inferenz, Unions können zu breit sein
json-schema-to-typescript JSON Schema Vertragsgetrieben und reproduzierbar Erfordert zuerst ein vertrauenswürdiges Schema

Ein typischer quicktype-Aufruf:

npx quicktype -s json -o user.ts user.json --just-types

--just-types gibt nur Typen und keinen Laufzeitkonvertierungscode aus, was zu Projekten passt, die bereits eine Parsing-Schicht haben. Ist das Schema handgeschrieben oder vom Backend erzeugt, passt json-schema-to-typescript besser, weil es required, enum und additionalProperties respektiert – Beschränkungen, die eine Beispielinferenz schlicht nicht sehen kann.

Die Schemagenerierung hat ihre eigenen Fallen, etwa jedes Feld als erforderlich zu markieren oder null stillschweigend in optional zu verwandeln; siehe häufige Fallstricke von JSON-Schema-Generatoren.

8. Eine Checkliste vor dem Commit generierter Typen

Gehen Sie diese fünf Punkte durch, bevor Sie die Ausgabe committen:

  1. Feldnamen normalisieren: Lassen Sie userId und user_id nicht nebeneinander existieren; einmal in einer Konvertierungsschicht abbilden.
  2. Optionalität nachprüfen: Mit echten Antworten vergleichen und herausfinden, welche Felder fehlen oder null sein können.
  3. Daten und IDs als Strings behalten: Einheit und Format im Typ dokumentieren und an der Verwendungsstelle ausdrücklich parsen.
  4. any und unknown reduzieren: Wo der Generator any ausgegeben hat, entweder einen Typ ergänzen oder unknown behalten und einen Guard hinzufügen.
  5. Laufzeitvalidierung ergänzen: Externe Daten mindestens einmal validieren; erwarten Sie nicht, dass ein Interface schlechte Daten abfängt.

Verwendet das Projekt mehrere JSON-Bibliotheken (JSON.parse, superjson, lossless-json), unterscheidet sich ihr Verhalten bei großen Ganzzahlen und Daten, bestätigen Sie es also bei der Auswahl. Siehe Auswahl der JSON-Parsing-Bibliothek.

Warum markiert der Generator jedes optionale Feld als erforderlich?

Er hat ein einzelnes Beispiel gesehen, jeder erschienene Schlüssel gilt also als immer vorhanden. Liefern Sie mehrere Beispiele, die verschiedene Fälle abdecken, oder generieren Sie aus JSON Schema – das required-Array dort ist die maßgebliche Quelle.

Warum scheitert die Deklaration von Date in einem Interface zur Laufzeit?

JSON.parse erzeugt nur Strings, Zahlen, Booleans, null, Objekte und Arrays; es konstruiert nie ein Date. Date zu deklarieren ist ein Versprechen zur Kompilierzeit, während der Laufzeitwert ein String bleibt. Behalten Sie string in der Transportschicht und konvertieren Sie an der Grenze.

Können generierte Typen als Validierung verwendet werden?

Nein. TypeScript-Typen werden nach der Kompilierung gelöscht und erlegen Laufzeitdaten nichts auf. Validieren Sie externe Daten mit zod, valibot, ajv oder einem handgeschriebenen Type Guard.

Sollte eine große Ganzzahl-id als number oder string typisiert werden?

Kann sie 2^53 übersteigen, muss sie string sein. JSON.parse verliert beim Parsen Präzision, und sie als number zu typisieren schreibt den Bug nur in den Typ, wo er nicht mehr zu reparieren ist.