JSON vers TypeScript : Générer des types à partir des réponses API
Si vous avez dĂ©jĂ appelĂ© une API REST en TypeScript et typĂ© la rĂ©ponse comme any, vous connaissez la douleur qui s'ensuit. L'autocomplĂ©tion disparaĂźt. Les fautes de frappe passent inaperçues. Un champ que vous pensiez ĂȘtre un string s'avĂšre ĂȘtre null, et votre application plante en production.
La solution ? Convertir vos réponses JSON en types TypeScript appropriés. Dans ce guide, nous verrons le mapping manuel, les outils automatisés et les bonnes pratiques qui rendent vos intégrations API solides comme le roc.
Pourquoi les types TypeScript issus du JSON sont importants
Le systÚme de types de TypeScript existe pour détecter les erreurs avant qu'elles n'atteignent vos utilisateurs. Lorsque vous modélisez une réponse API avec des types précis, trois choses se produisent :
- L'autocomplétion fonctionne partout. Votre éditeur connaßt la structure des données, vous n'avez plus à deviner les noms de champs.
- Les refactorisations deviennent sûres. Renommez une propriété et le compilateur vous indique chaque endroit qui casse.
- Les erreurs d'exécution diminuent. Vous gérez
null,undefinedet les structures inattendues Ă la frontiĂšre plutĂŽt qu'au cĆur de votre application.
Sans types, vous écrivez essentiellement du JavaScript avec des étapes supplémentaires. Avec des types, vous obtenez un contrat vivant entre votre code et les données qu'il consomme.
Mapping manuel JSON vers TypeScript
Commençons par les fondamentaux. Ătant donnĂ© un objet JSON, comment le transformer en interface TypeScript ?
Objets basiques
Voici une réponse utilisateur simple provenant d'une API :
{
"id": 42,
"username": "jdoe",
"email": "jdoe@example.com",
"isActive": true
}
L'interface TypeScript correspondante :
interface User {
id: number;
username: string;
email: string;
isActive: boolean;
}
Chaque valeur JSON correspond à un type primitif TypeScript : number, string ou boolean. Les clés d'objet deviennent des noms de propriétés.
Objets imbriqués
Les API réelles retournent rarement des structures plates. Considérons un utilisateur avec une adresse :
{
"id": 42,
"username": "jdoe",
"address": {
"street": "123 Main St",
"city": "Springfield",
"zipCode": "62704",
"country": "US"
}
}
Séparez les objets imbriqués dans leurs propres interfaces :
interface Address {
street: string;
city: string;
zipCode: string;
country: string;
}
interface User {
id: number;
username: string;
address: Address;
}
Cela garde chaque type ciblĂ© et rĂ©utilisable. Si un autre endpoint retourne Ă©galement une Address, vous utilisez la mĂȘme interface.
Tableaux
Lorsqu'un champ contient un tableau, vous annotez le type de l'élément :
{
"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[];
}
Utilisez la syntaxe Type[] pour les cas simples. Pour les gĂ©nĂ©riques complexes, Array<Type> peut ĂȘtre plus clair, mais la cohĂ©rence importe plus que le choix.
Champs optionnels
Les API retournent souvent des champs qui peuvent ĂȘtre prĂ©sents ou non. Marquez-les avec ? :
// Parfois l'API retourne bio, parfois non
{
"id": 42,
"username": "jdoe",
"bio": "Software developer"
}
interface User {
id: number;
username: string;
bio?: string; // Peut ĂȘtre absent de la rĂ©ponse
}
Un champ optionnel (bio?: string) signifie que la clĂ© peut ĂȘtre entiĂšrement absente. C'est diffĂ©rent d'un champ nullable, oĂč la clĂ© existe mais la valeur peut ĂȘtre null.
Gestion des cas limites
Les API en production sont désordonnées. Voici comment gérer les piÚges courants.
Champs nullables
Lorsqu'un champ peut ĂȘtre explicitement null :
{
"id": 42,
"username": "jdoe",
"avatarUrl": null,
"deletedAt": null
}
interface User {
id: number;
username: string;
avatarUrl: string | null;
deletedAt: string | null; // ChaĂźne de date ISO ou null
}
Si un champ peut ĂȘtre Ă la fois absent et null, combinez les deux :
interface User {
id: number;
username: string;
middleName?: string | null; // Peut ĂȘtre absent, ou prĂ©sent mais null
}
Types union
Certains champs acceptent plusieurs types. Une API de recherche peut retourner des résultats mixtes :
{
"results": [
{ "type": "user", "id": 1, "username": "jdoe" },
{ "type": "post", "id": 2, "title": "Hello" }
]
}
Modélisez cela avec une union discriminée :
interface UserResult {
type: 'user';
id: number;
username: string;
}
interface PostResult {
type: 'post';
id: number;
title: string;
}
type SearchResult = UserResult | PostResult;
interface SearchResponse {
results: SearchResult[];
}
Le champ type agit comme discriminant. TypeScript affine le type automatiquement lorsque vous le vérifiez :
function renderResult(result: SearchResult) {
if (result.type === 'user') {
console.log(result.username); // TypeScript sait que c'est UserResult
} else {
console.log(result.title); // TypeScript sait que c'est PostResult
}
}
ĂnumĂ©rations Ă partir de valeurs string
Lorsqu'une API retourne un ensemble fixe de valeurs string, utilisez des unions de littéraux string plutÎt que des enums TypeScript :
{ "status": "active" }
// Valeurs possibles : "active", "inactive", "suspended"
type UserStatus = 'active' | 'inactive' | 'suspended';
interface User {
id: number;
status: UserStatus;
}
Les unions de littĂ©raux string sont plus simples, optimisables par le tree-shaking, et ne gĂ©nĂšrent pas de JavaScript Ă l'exĂ©cution contrairement Ă enum. RĂ©servez enum aux cas oĂč vous avez besoin du mapping inverse (nombre vers nom).
Interfaces vs alias de types
interface et type peuvent tous deux décrire des formes d'objets. Voici quand choisir l'un ou l'autre :
Utilisez interface pour les formes d'objets que vous pourriez étendre :
interface BaseEntity {
id: number;
createdAt: string;
updatedAt: string;
}
interface User extends BaseEntity {
username: string;
email: string;
}
Utilisez type pour les unions, intersections et types calculés :
type ApiResponse<T> = {
data: T;
meta: { total: number; page: number };
};
type UserOrAdmin = User | Admin;
En pratique, les deux fonctionnent dans la plupart des cas. Choisissez une convention pour votre projet et tenez-vous-y. Beaucoup d'équipes utilisent interface pour les modÚles de données et type pour tout le reste.
Utilisation des outils automatisés
Typer manuellement les interfaces fonctionne pour les petites API, mais devient fastidieux quand vous traitez des endpoints qui retournent plus de 50 champs ou des structures profondément imbriquées.
Avant de convertir du JSON en types, assurez-vous que votre JSON est valide et bien formaté. Notre formateur JSON nettoie les réponses API désordonnées, et le validateur JSON détecte les erreurs de syntaxe avant qu'elles ne vous fassent perdre du temps.
Convertisseurs en ligne
Des outils comme quicktype, json2ts et les convertisseurs JSON-to-TypeScript vous permettent de coller un payload JSON et d'obtenir des interfaces instantanément. Ils gÚrent automatiquement les objets imbriqués, les tableaux et les champs optionnels.
Le flux de travail typique :
- Appelez votre API et copiez la réponse
- Collez le JSON dans le convertisseur
- Vérifiez les types générés et ajustez les noms
- Copiez le TypeScript dans votre base de code
Génération de code à partir de spécifications OpenAPI
Si votre API dispose d'une spécification OpenAPI (Swagger), évitez complÚtement la conversion JSON. Des outils comme openapi-typescript génÚrent les types directement à partir de la spécification :
npx openapi-typescript https://api.example.com/openapi.json -o ./src/types/api.ts
Cette approche est supérieure car la spécification définit explicitement les champs optionnels, les types nullables et les énumérations. Vous obtenez des types précis sans avoir à deviner à partir de données d'exemple.
Validation à l'exécution avec Zod
Pour une sécurité maximale, définissez vos formes avec Zod et inférez les types TypeScript à partir d'elles :
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>;
// Maintenant validez à l'exécution
const response = await fetch('/api/users/42');
const data = await response.json();
const user = UserSchema.parse(data); // Lance une erreur si la forme ne correspond pas
Avec Zod, vos types et votre logique de validation cohabitent au mĂȘme endroit. Si l'API change de forme, parse() lance immĂ©diatement une erreur au lieu de laisser des donnĂ©es incorrectes se propager dans votre application.
Intégration des types avec les appels API
Les types ne sont utiles que si vous les appliquez Ă la frontiĂšre oĂč les donnĂ©es entrent dans votre application.
Wrapper Fetch typé
Créez une fonction fetch générique qui impose les types :
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;
}
// Utilisation
const user = await fetchJson<User>('/api/users/42');
const validatedUser = await fetchJson('/api/users/42', UserSchema);
Axios avec types
Si vous utilisez Axios, appliquez les types au générique de la réponse :
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; // EntiÚrement typé
}
React Query / TanStack Query
Dans les applications React, associez les types à votre bibliothÚque de récupération de données :
import { useQuery } from '@tanstack/react-query';
function useUser(id: number) {
return useQuery({
queryKey: ['user', id],
queryFn: () => fetchJson<User>(`/api/users/${id}`),
});
}
// Dans votre composant
const { data: user, isLoading } = useUser(42);
// user est typé comme User | undefined
Bonnes pratiques
Activer les vérifications strictes de null
Dans votre tsconfig.json, activez le mode strict (ou au minimum strictNullChecks). Cela vous oblige à gérer null et undefined explicitement :
{
"compilerOptions": {
"strict": true
}
}
Sans les vĂ©rifications strictes de null, TypeScript vous laisse accĂ©der Ă .username sur une valeur potentiellement null sans se plaindre. Cela annule tout l'intĂ©rĂȘt de typer vos rĂ©ponses API.
Utiliser readonly pour les données immuables
Les réponses API sont des données que vous avez reçues, pas des données que vous devriez modifier. Marquez les propriétés comme readonly :
interface User {
readonly id: number;
readonly username: string;
readonly email: string;
readonly createdAt: string;
}
Ou utilisez un type utilitaire pour l'interface entiĂšre :
type ImmutableUser = Readonly<User>;
Cela empĂȘche les mutations accidentelles qui causent des bugs subtils.
Types marqués pour les identifiants
EmpĂȘchez la confusion entre les identifiants de diffĂ©rentes entitĂ©s avec des types marquĂ©s :
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); // Erreur de type ! Impossible de passer PostId lĂ oĂč UserId est attendu
Centralisez vos types
Conservez tous les types liés aux API dans un répertoire dédié :
src/
types/
api/
user.ts
post.ts
common.ts // Types partagés comme PaginatedResponse
index.ts // Réexportations
Cela rend les types faciles Ă trouver, importer et maintenir. Lorsqu'une API change, vous mettez Ă jour un seul fichier et le compilateur vous montre tout ce qui casse.
Valider Ă la frontiĂšre
Ne faites jamais confiance aux donnĂ©es externes. MĂȘme avec des types parfaits, l'API peut retourner quelque chose d'inattendu. Validez les rĂ©ponses lĂ oĂč elles entrent dans votre application :
// Ne faites pas ça
const user = (await response.json()) as User; // Fait confiance aveuglément
// Faites plutÎt ça
const data = await response.json();
const user = UserSchema.parse(data); // Valide la forme
Les assertions de type (as User) font taire le compilateur mais ne vérifient pas les données. La validation à l'exécution (Zod, io-ts, Valibot) vérifie réellement que la forme correspond.
Travailler avec des structures JSON complexes
Lorsque vous traitez des payloads JSON volumineux ou complexes, utilisez l'éditeur JSON pour explorer et comprendre la structure avant d'écrire les types. Il est bien plus facile de voir les relations imbriquées dans un éditeur adapté que dans du texte brut.
Pour les équipes travaillant avec des API JSON, consultez ces guides connexes :
- Bonnes pratiques de formatage JSON â gardez votre JSON propre et lisible
- Guide de validation JSON Schema â validez les contrats API avec JSON Schema
- Patterns de conception d'API JSON â concevez des API faciles Ă typer
- Comment valider du JSON â dĂ©tectez les payloads malformĂ©s tĂŽt
Assembler le tout
Voici un exemple complet qui rassemble tous les concepts â du JSON brut aux donnĂ©es validĂ©es et typĂ©es dans un composant React :
import { z } from 'zod';
// 1. Définir le schéma (source unique de vérité)
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. Inférer le type à partir du schéma
type User = z.infer<typeof UserSchema>;
// 3. Créer une fonction fetch typée
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. Utiliser dans votre application avec une sécurité de type complÚte
const user = await getUser(42);
console.log(user.profile.bio); // string | null â le compilateur le sait
console.log(user.profile.socialLinks); // tableau ou undefined â le compilateur le sait
Pas de any. Pas de devinettes. Pas de surprises à l'exécution.
Conclusion
Convertir du JSON en types TypeScript ne consiste pas seulement à satisfaire le compilateur. C'est construire un filet de sécurité qui détecte les erreurs tÎt, alimente l'autocomplétion de votre éditeur et rend votre base de code plus facile à maintenir.
Commencez simplement : copiez une réponse JSON, écrivez l'interface manuellement et typez vos appels fetch. Au fur et à mesure que votre projet grandit, adoptez Zod pour la validation à l'exécution et la génération de code OpenAPI pour les grandes API. L'investissement initial se rentabilise dÚs la premiÚre fois qu'une erreur de type attrape un bug qui aurait atteint la production.