JSON zu TypeScript: Typen aus API-Antworten generieren
Wenn Sie jemals eine REST-API in TypeScript aufgerufen und die Antwort als any typisiert haben, kennen Sie den Schmerz, der darauf folgt. Die AutovervollstĂ€ndigung verschwindet. Tippfehler schleichen sich ein. Ein Feld, das Sie fĂŒr einen string hielten, entpuppt sich als null, und Ihre App stĂŒrzt in der Produktion ab.
Die Lösung? Konvertieren Sie Ihre JSON-Antworten in ordentliche TypeScript-Typen. In diesem Leitfaden gehen wir manuelles Mapping, automatisierte Tools und die Best Practices durch, die Ihre API-Integrationen absolut robust machen.
Warum TypeScript-Typen aus JSON wichtig sind
Das Typsystem von TypeScript existiert, um Fehler abzufangen, bevor sie Ihre Benutzer erreichen. Wenn Sie eine API-Antwort mit genauen Typen modellieren, passieren drei Dinge:
- Die AutovervollstĂ€ndigung funktioniert ĂŒberall. Ihr Editor kennt die Struktur der Daten, sodass Sie aufhören, Feldnamen zu erraten.
- Refactoring wird sicher. Benennen Sie eine Eigenschaft um, und der Compiler zeigt Ihnen jede Stelle, die bricht.
- Laufzeitfehler sinken. Sie behandeln
null,undefinedund unerwartete Strukturen an der Grenzschicht, anstatt tief in Ihrer App.
Ohne Typen schreiben Sie im Grunde JavaScript mit zusÀtzlichen Schritten. Mit Typen erhalten Sie einen lebenden Vertrag zwischen Ihrem Code und den Daten, die er konsumiert.
Manuelles JSON-zu-TypeScript-Mapping
Beginnen wir mit den Grundlagen. Wie verwandeln Sie ein gegebenes JSON-Objekt in ein TypeScript-Interface?
Einfache Objekte
Hier ist eine einfache Benutzerantwort von einer API:
{
"id": 42,
"username": "jdoe",
"email": "jdoe@example.com",
"isActive": true
}
Das entsprechende TypeScript-Interface:
interface User {
id: number;
username: string;
email: string;
isActive: boolean;
}
Jeder JSON-Wert wird einem TypeScript-Primitive zugeordnet: number, string oder boolean. ObjektschlĂŒssel werden zu Eigenschaftsnamen.
Verschachtelte Objekte
Echte APIs geben selten flache Strukturen zurĂŒck. Betrachten Sie einen Benutzer mit einer Adresse:
{
"id": 42,
"username": "jdoe",
"address": {
"street": "123 Main St",
"city": "Springfield",
"zipCode": "62704",
"country": "US"
}
}
Teilen Sie verschachtelte Objekte in eigene Interfaces auf:
interface Address {
street: string;
city: string;
zipCode: string;
country: string;
}
interface User {
id: number;
username: string;
address: Address;
}
Das hĂ€lt jeden Typ fokussiert und wiederverwendbar. Wenn ein anderer Endpunkt ebenfalls eine Address zurĂŒckgibt, verwenden Sie dasselbe Interface.
Arrays
Wenn ein Feld ein Array enthÀlt, annotieren Sie den Elementtyp:
{
"id": 42,
"username": "jdoe",
"roles": ["admin", "editor"],
"posts": [
{ "id": 1, "title": "Hello World" },
{ "id": 2, "title": "TypeScript Tips" }
]
}
interface Post {
id: number;
title: string;
}
interface User {
id: number;
username: string;
roles: string[];
posts: Post[];
}
Verwenden Sie die Type[]-Syntax fĂŒr einfache FĂ€lle. FĂŒr komplexe Generics kann Array<Type> klarer sein, aber Konsistenz ist wichtiger als die Wahl der Variante.
Optionale Felder
APIs geben oft Felder zurĂŒck, die vorhanden sein können oder nicht. Markieren Sie diese mit ?:
// Manchmal gibt die API bio zurĂŒck, manchmal nicht
{
"id": 42,
"username": "jdoe",
"bio": "Software developer"
}
interface User {
id: number;
username: string;
bio?: string; // Kann in der Antwort fehlen
}
Ein optionales Feld (bio?: string) bedeutet, dass der SchlĂŒssel komplett fehlen könnte. Das unterscheidet sich von einem nullbaren Feld, bei dem der SchlĂŒssel existiert, der Wert aber null sein könnte.
Umgang mit SonderfÀllen
Produktions-APIs sind unordentlich. So gehen Sie mit den hÀufigsten Stolperfallen um.
Nullbare Felder
Wenn ein Feld explizit null sein kann:
{
"id": 42,
"username": "jdoe",
"avatarUrl": null,
"deletedAt": null
}
interface User {
id: number;
username: string;
avatarUrl: string | null;
deletedAt: string | null; // ISO-Datumsstring oder null
}
Wenn ein Feld sowohl fehlen als auch null sein kann, kombinieren Sie beides:
interface User {
id: number;
username: string;
middleName?: string | null; // Kann fehlen oder vorhanden aber null sein
}
Union-Typen
Manche Felder akzeptieren mehrere Typen. Eine Such-API könnte gemischte Ergebnisse zurĂŒckgeben:
{
"results": [
{ "type": "user", "id": 1, "username": "jdoe" },
{ "type": "post", "id": 2, "title": "Hello" }
]
}
Modellieren Sie dies mit einer diskriminierten Union:
interface UserResult {
type: 'user';
id: number;
username: string;
}
interface PostResult {
type: 'post';
id: number;
title: string;
}
type SearchResult = UserResult | PostResult;
interface SearchResponse {
results: SearchResult[];
}
Das type-Feld fungiert als Diskriminator. TypeScript verengt den Typ automatisch, wenn Sie ihn prĂŒfen:
function renderResult(result: SearchResult) {
if (result.type === 'user') {
console.log(result.username); // TypeScript weiĂ, dass dies UserResult ist
} else {
console.log(result.title); // TypeScript weiĂ, dass dies PostResult ist
}
}
Enums aus String-Werten
Wenn eine API eine feste Menge von String-Werten zurĂŒckgibt, verwenden Sie String-Literal-Unions anstelle von TypeScript-Enums:
{ "status": "active" }
// Mögliche Werte: "active", "inactive", "suspended"
type UserStatus = 'active' | 'inactive' | 'suspended';
interface User {
id: number;
status: UserStatus;
}
String-Literal-Unions sind einfacher, Tree-Shaking-fĂ€hig und erzeugen kein Laufzeit-JavaScript wie enum. Reservieren Sie enum fĂŒr FĂ€lle, in denen Sie Reverse-Mapping (Zahl zu Name) benötigen.
Interfaces vs. Type-Aliase
Sowohl interface als auch type können Objektformen beschreiben. So entscheiden Sie, wann Sie was verwenden:
Verwenden Sie interface fĂŒr Objektformen, die Sie möglicherweise erweitern:
interface BaseEntity {
id: number;
createdAt: string;
updatedAt: string;
}
interface User extends BaseEntity {
username: string;
email: string;
}
Verwenden Sie type fĂŒr Unions, Intersections und berechnete Typen:
type ApiResponse<T> = {
data: T;
meta: { total: number; page: number };
};
type UserOrAdmin = User | Admin;
In der Praxis funktioniert beides fĂŒr die meisten FĂ€lle. WĂ€hlen Sie eine Konvention fĂŒr Ihr Projekt und bleiben Sie dabei. Viele Teams verwenden interface fĂŒr Datenmodelle und type fĂŒr alles andere.
Automatisierte Tools verwenden
Manuelles Schreiben von Interfaces funktioniert fĂŒr kleine APIs, wird aber mĂŒhsam, wenn Sie es mit Endpunkten zu tun haben, die 50+ Felder oder tief verschachtelte Strukturen zurĂŒckgeben.
Bevor Sie JSON in Typen konvertieren, stellen Sie sicher, dass Ihr JSON gĂŒltig und gut formatiert ist. Unser JSON-Formatierer bereinigt unordentliche API-Antworten, und der JSON-Validator erkennt Syntaxfehler, bevor sie Ihre Zeit verschwenden.
Online-Konverter
Tools wie quicktype, json2ts und JSON-zu-TypeScript-Konverter ermöglichen es Ihnen, eine JSON-Payload einzufĂŒgen und sofort Interfaces zurĂŒckzubekommen. Sie verarbeiten verschachtelte Objekte, Arrays und optionale Felder automatisch.
Der typische Arbeitsablauf:
- Rufen Sie Ihre API auf und kopieren Sie die Antwort
- FĂŒgen Sie das JSON in den Konverter ein
- ĂberprĂŒfen Sie die generierten Typen und passen Sie die Benennung an
- Kopieren Sie das TypeScript in Ihre Codebasis
Code-Generierung aus OpenAPI-Spezifikationen
Wenn Ihre API eine OpenAPI-Spezifikation (Swagger) hat, ĂŒberspringen Sie die JSON-Konvertierung komplett. Tools wie openapi-typescript generieren Typen direkt aus der Spezifikation:
npx openapi-typescript https://api.example.com/openapi.json -o ./src/types/api.ts
Dieser Ansatz ist ĂŒberlegen, da die Spezifikation optionale Felder, nullbare Typen und Enums explizit definiert. Sie erhalten genaue Typen, ohne aus Beispieldaten raten zu mĂŒssen.
Laufzeitvalidierung mit Zod
FĂŒr maximale Sicherheit definieren Sie Ihre Formen mit Zod und leiten TypeScript-Typen daraus ab:
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
username: z.string(),
email: z.string().email(),
isActive: z.boolean(),
bio: z.string().nullable().optional(),
});
type User = z.infer<typeof UserSchema>;
// Jetzt zur Laufzeit validieren
const response = await fetch('/api/users/42');
const data = await response.json();
const user = UserSchema.parse(data); // Wirft einen Fehler, wenn die Form nicht passt
Mit Zod leben Ihre Typen und Validierungslogik an einem Ort. Wenn die API ihre Form Àndert, wirft parse() sofort einen Fehler, anstatt fehlerhafte Daten durch Ihre App propagieren zu lassen.
Typen in API-Aufrufe integrieren
Typen sind nur nĂŒtzlich, wenn Sie sie an der Grenzschicht anwenden, wo Daten in Ihre Anwendung eintreten.
Typisierter Fetch-Wrapper
Erstellen Sie eine generische Fetch-Funktion, die Typen erzwingt:
async function fetchJson<T>(url: string, schema?: z.ZodType<T>): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (schema) {
return schema.parse(data);
}
return data as T;
}
// Verwendung
const user = await fetchJson<User>('/api/users/42');
const validatedUser = await fetchJson('/api/users/42', UserSchema);
Axios mit Typen
Wenn Sie Axios verwenden, wenden Sie Typen auf den Response-Generic an:
import axios from 'axios';
interface PaginatedResponse<T> {
data: T[];
meta: {
total: number;
page: number;
perPage: number;
lastPage: number;
};
}
async function getUsers(page: number = 1) {
const response = await axios.get<PaginatedResponse<User>>('/api/users', {
params: { page },
});
return response.data; // VollstÀndig typisiert
}
React Query / TanStack Query
In React-Apps kombinieren Sie Typen mit Ihrer Datenabruf-Bibliothek:
import { useQuery } from '@tanstack/react-query';
function useUser(id: number) {
return useQuery({
queryKey: ['user', id],
queryFn: () => fetchJson<User>(`/api/users/${id}`),
});
}
// In Ihrer Komponente
const { data: user, isLoading } = useUser(42);
// user ist typisiert als User | undefined
Best Practices
Strikte Null-PrĂŒfungen aktivieren
Aktivieren Sie in Ihrer tsconfig.json den strict-Modus (oder mindestens strictNullChecks). Dies zwingt Sie, null und undefined explizit zu behandeln:
{
"compilerOptions": {
"strict": true
}
}
Ohne strikte Null-PrĂŒfungen lĂ€sst TypeScript Sie auf .username bei einem potenziell null-Wert zugreifen, ohne sich zu beschweren. Das macht den ganzen Zweck der Typisierung Ihrer API-Antworten zunichte.
readonly fĂŒr unverĂ€nderliche Daten verwenden
API-Antworten sind Daten, die Sie erhalten haben, nicht Daten, die Sie verÀndern sollten. Markieren Sie Eigenschaften als readonly:
interface User {
readonly id: number;
readonly username: string;
readonly email: string;
readonly createdAt: string;
}
Oder verwenden Sie einen Utility-Typ fĂŒr das gesamte Interface:
type ImmutableUser = Readonly<User>;
Dies verhindert versehentliche Mutationen, die subtile Fehler verursachen.
Branded Types fĂŒr IDs
Verhindern Sie das Verwechseln von IDs verschiedener EntitÀten mit Branded Types:
type UserId = number & { readonly __brand: 'UserId' };
type PostId = number & { readonly __brand: 'PostId' };
function getUser(id: UserId): Promise<User> { /* ... */ }
function getPost(id: PostId): Promise<Post> { /* ... */ }
const userId = 42 as UserId;
const postId = 7 as PostId;
getUser(userId); // OK
getUser(postId); // Typfehler! PostId kann nicht ĂŒbergeben werden, wo UserId erwartet wird
Typen zentralisieren
Bewahren Sie alle API-bezogenen Typen in einem dedizierten Verzeichnis auf:
src/
types/
api/
user.ts
post.ts
common.ts // Gemeinsame Typen wie PaginatedResponse
index.ts // Re-Exports
Das macht Typen leicht auffindbar, importierbar und wartbar. Wenn sich eine API Àndert, aktualisieren Sie eine Datei, und der Compiler zeigt Ihnen alles, was bricht.
An der Grenzschicht validieren
Vertrauen Sie niemals externen Daten. Selbst mit perfekten Typen könnte die API etwas Unerwartetes zurĂŒckgeben. Validieren Sie Antworten dort, wo sie in Ihre App eintreten:
// Nicht so
const user = (await response.json()) as User; // Vertraut blind
// Sondern so
const data = await response.json();
const user = UserSchema.parse(data); // Validiert die Struktur
Typ-Assertions (as User) bringen den Compiler zum Schweigen, prĂŒfen aber nicht die Daten. Laufzeitvalidierung (Zod, io-ts, Valibot) verifiziert tatsĂ€chlich, ob die Struktur ĂŒbereinstimmt.
Arbeiten mit komplexen JSON-Strukturen
Wenn Sie mit groĂen oder komplexen JSON-Payloads arbeiten, verwenden Sie den JSON-Editor, um die Struktur zu erkunden und zu verstehen, bevor Sie Typen schreiben. Es ist viel einfacher, verschachtelte Beziehungen in einem richtigen Editor zu erkennen als in Rohtext.
FĂŒr Teams, die mit JSON-APIs arbeiten, schauen Sie sich diese verwandten LeitfĂ€den an:
- JSON-Formatierung Best Practices â halten Sie Ihr JSON sauber und lesbar
- JSON-Schema-Validierung Leitfaden â validieren Sie API-VertrĂ€ge mit JSON-Schema
- JSON-API-Design-Patterns â entwerfen Sie APIs, die leicht zu typisieren sind
- So validieren Sie JSON â erkennen Sie fehlerhafte Payloads frĂŒhzeitig
Alles zusammenfĂŒgen
Hier ist ein vollstĂ€ndiges Beispiel, das alles zusammenbringt â von rohem JSON zu validierten, typisierten Daten in einer React-Komponente:
import { z } from 'zod';
// 1. Schema definieren (einzige Quelle der Wahrheit)
const UserSchema = z.object({
id: z.number(),
username: z.string(),
email: z.string().email(),
isActive: z.boolean(),
role: z.enum(['admin', 'editor', 'viewer']),
profile: z.object({
bio: z.string().nullable(),
avatarUrl: z.string().url().nullable(),
socialLinks: z.array(z.object({
platform: z.string(),
url: z.string().url(),
})).optional(),
}),
});
// 2. Typ aus dem Schema ableiten
type User = z.infer<typeof UserSchema>;
// 3. Typisierte Fetch-Funktion erstellen
async function getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`Failed to fetch user ${id}`);
const data = await response.json();
return UserSchema.parse(data);
}
// 4. In Ihrer Anwendung mit voller Typsicherheit verwenden
const user = await getUser(42);
console.log(user.profile.bio); // string | null â der Compiler weiĂ es
console.log(user.profile.socialLinks); // Array oder undefined â der Compiler weiĂ es
Kein any. Kein Raten. Keine LaufzeitĂŒberraschungen.
Fazit
JSON in TypeScript-Typen zu konvertieren dient nicht nur dazu, den Compiler zufriedenzustellen. Es geht darum, ein Sicherheitsnetz aufzubauen, das Fehler frĂŒhzeitig abfĂ€ngt, die AutovervollstĂ€ndigung Ihres Editors antreibt und Ihre Codebasis leichter wartbar macht.
Fangen Sie einfach an: Kopieren Sie eine JSON-Antwort, schreiben Sie das Interface manuell und typisieren Sie Ihre Fetch-Aufrufe. Wenn Ihr Projekt wĂ€chst, setzen Sie Zod fĂŒr die Laufzeitvalidierung und OpenAPI-Codegen fĂŒr groĂe APIs ein. Die anfĂ€ngliche Investition zahlt sich beim ersten Mal aus, wenn ein Typfehler einen Bug abfĂ€ngt, der sonst die Produktion erreicht hĂ€tte.