JSON Toolkit

JSON vers TypeScript : interfaces, gardes de type et limites de la génération

L’essentiel : Générer une interface à partir d'un JSON d'exemple ne fait que la moitié du travail : champs facultatifs, null, unions, dates et grands entiers demandent une passe humaine, et les types ne font aucune validation à l'exécution.

Intermédiaire~9 min de lecturePublié le 2026-10-31Mis à jour le 2026-11-04
Illustration du guide JSON vers interface TypeScript et gardes de type

L'essentiel : générez l'interface pour gagner de la frappe, puis revérifiez le caractère facultatif et null, gardez les dates et grands entiers en chaînes, et ajoutez une validation à l'exécution à la frontière ; traiter les types générés comme le contrat ne vous donne aucune validation.

Un générateur ne voit que l'unique échantillon que vous lui collez. Savoir si des champs peuvent manquer, si des valeurs peuvent être null et quel format les dates utilisent relève de la supposition — et les mauvaises suppositions sont à corriger par vous.

1. Ce que fait réellement le générateur

Collez un échantillon JSON dans JSON vers TypeScript et l'interface obtenue est un résumé de forme : il compte les clés et types de valeurs qu'il a vus, puis choisit la représentation la plus étroite disponible.

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

La sortie ressemble à peu près à ceci :

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

Cela compile, mais décrit une valeur idéale, pas le vrai contrat du point de terminaison. Le générateur ne sait pas si email peut manquer, si tags peut être vide, ni si createdAt porte toujours un fuseau horaire. Il ne sait même pas que id pourrait être une chaîne dans d'autres enregistrements.

La bonne attente est donc : le générateur épargne la frappe, l'humain fournit le sens.

2. Champs facultatifs et null : ? n'est pas | null

C'est la partie de la sortie générée la plus souvent copiée, et celle qui casse le plus souvent.

Forme TypeScript Signification JSON correspondant
email: string Toujours présent, toujours une chaîne {"email":"a@b.c"}
email?: string La clé peut être absente ; si présente, elle ne peut pas être null {} ou {"email":"a@b.c"}
email: string | null La clé est toujours présente, la valeur peut être null {"email":null}
email?: string | null La clé peut être absente et la valeur peut être null Les trois cas

Le générateur a vu un seul échantillon, donc il produit généralement la première forme. Si l'API peut réellement renvoyer null, le type ment : tout compile, puis email.toLowerCase() lève une exception à l'exécution.

Deux façons de corriger :

  1. Collectez plusieurs échantillons réels couvrant à la fois « clé absente » et « valeur null », puis laissez le générateur réinférer.
  2. Après génération, modifiez les champs à la main en ?: T | null et traitez réellement les deux branches dans le code.

Ne prenez pas le raccourci de tout marquer ?: string. Facultatif signifie « la clé peut ne pas exister », pas « la valeur peut être vide ». undefined et null se comportent aussi différemment dans JSON.stringify : le premier est omis, le second est conservé comme null.

3. Éléments de tableau hétérogènes et types union

Les tableaux sont la structure la plus facile à rater. Si tags apparaît comme ["admin"], [] et ["beta", 3] dans trois enregistrements, le générateur peut émettre string[], (string | number)[] ou unknown[], selon les échantillons qu'il a vus.

Un humain doit décider :

  • Les types d'éléments sont réellement mixtes : écrivez (string | number)[] et rétrécissez au point d'utilisation.
  • Un seul enregistrement est sale : corrigez les données. Ne le masquez pas avec any[].
  • Tableaux vides : le générateur ne peut pas inférer de type d'élément et retombe sur never[] ou any[], donc il faut le remplir.
  • Tuples de longueur fixe : utilisez [string, string] seulement quand le métier garantit réellement la longueur ; sinon ajouter un élément plus tard devient une erreur de compilation.

Les tableaux d'objets demandent aussi attention quand les clés sont inégales :

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

Placez le caractère facultatif sur le type d'élément, pas sur le tableau. items?: OrderItem[] signifie que le tableau lui-même peut être absent, ce qui est une affirmation complètement différente.

4. Dates, grands entiers, énumérations : des types que JSON n'a pas

JSON n'a que des chaînes et des nombres ; le sens métier vit dans des conventions. Le générateur infère à partir des littéraux, donc corrigez à la main :

Littéral JSON Sortie générée habituelle Ce que cela devrait être
"2026-10-31T08:00:00Z" string Gardez string dans la couche de transport et convertissez au point d'utilisation ; ne déclarez jamais Date directement
1717075200000 number Utilisez number pour un horodatage en millisecondes et documentez l'unité
"1234567890123456789" string Gardez une string pour que number ne perde pas de précision
1 et "1" mélangés number | string Normalisez vers une seule forme ; ne poussez pas l'union dans le code métier
"admin" string Une union littérale 'admin' | 'editor', ou une constante d'énumération partagée
true et "true" mélangés boolean | string Corrigez la source de données au lieu de générer ce type

La règle la plus importante : les dates en JSON sont toujours des chaînes. Écrire createdAt: Date est une auto-illusion courante — JSON.parse ne construit jamais de Date, donc la valeur à l'exécution reste une chaîne et .getTime() lève une exception. Gardez le type de transport en string, appelez explicitement new Date(value) à la frontière, et vérifiez Number.isNaN.

Les grands entiers fonctionnent de la même manière. Une fois qu'un id dépasse 2^53, JSON.parse a déjà perdu de la précision, et générer number ne fait que cimenter le bug dans le type. Le backend renvoie une chaîne, le frontend la garde en chaîne ; c'est la seule option fiable.

5. Les types générés ne font aucune validation à l'exécution

Les types TypeScript sont effacés à la compilation. Quelle que soit la précision de l'interface, elle ne peut pas arrêter une réponse {"id": "x"} — elle contraint votre propre code, pas les octets qui arrivent sur le réseau.

/**
 * 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';
}

Une garde de type ne couvre que les champs que vous écrivez dans la vérification. À mesure que les champs se multiplient, les gardes écrites à la main deviennent une seconde définition à maintenir, qui dérive de l'interface.

Le compromis pratique ressemble à ceci :

Approche Validation à l'exécution Source des types Adaptée à
Interface seule Aucune Écrite à la main ou générée Données internes fiables, contraintes à la compilation
Interface plus garde écrite à la main Oui Deux définitions qui divergent Peu de champs, frontière claire
zod / valibot Oui Types inférés depuis le schéma API externes, formulaires, configuration
JSON Schema plus validateur Oui Types générés depuis le schéma Contrats inter-langages, documentation

Si les données viennent de l'extérieur, faites du schéma la source unique de vérité et dérivez les types avec z.infer ou json-schema-to-ts. Alors « la validation passe » et « le type est correct » sont le même fait, et il n'y a pas de seconde définition à faire diverger. Pour l'approche complète de synchronisation bidirectionnelle, voir Synchronisation entre schéma et types TypeScript.

6. Interface ou type : ne débattez pas sur la sortie générée

Les générateurs émettent généralement interface, mais les deux sont interchangeables dans la plupart des situations. Ce qui compte réellement, ce sont trois capacités :

  • Fusion de déclarations : interface peut être déclaré plusieurs fois et fusionne automatiquement ; type ne le peut pas. Cela aide quand on corrige des types tiers, mais les modèles métier devraient l'éviter — la fusion laisse passer des « champs superflus » inaperçus par le compilateur.
  • Unions et types mappés : type peut exprimer directement A | B, Partial<T> et les types de littéraux de gabarit ; interface ne le peut pas.
  • Récursion : les deux la prennent en charge, mais les formes récursives complexes se lisent plus naturellement en type.

Une règle d'équipe plus utile : utilisez interface pour les formes d'objet, type pour les unions, les types utilitaires et les types de fonction. Laissez la sortie générée à sa valeur par défaut et passez le temps de revue sur le caractère facultatif et la validation à l'exécution.

7. Chaîne d'outils : trois façons de générer

Approche Entrée Atout Limite
JSON vers TypeScript du site JSON d'exemple Fonctionne immédiatement, traité localement Basé sur l'échantillon courant uniquement ; rétrécissement manuel nécessaire
quicktype JSON / JSON Schema Sortie multi-langages, intégration CLI Inférence conservatrice, unions parfois trop larges
json-schema-to-typescript JSON Schema Piloté par le contrat et reproductible Exige d'abord un schéma fiable

Une invocation quicktype typique :

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

--just-types n'émet que des types et aucun code de conversion à l'exécution, ce qui convient aux projets qui ont déjà une couche de parsing. Si le schéma est écrit à la main ou produit par le backend, json-schema-to-typescript convient mieux car il respecte required, enum et additionalProperties — des contraintes que l'inférence à partir d'échantillons ne peut simplement pas voir.

La génération de schéma a ses propres pièges, comme marquer tous les champs obligatoires ou transformer silencieusement null en facultatif ; voir les pièges courants des générateurs JSON Schema.

8. Une liste de vérification avant de committer des types générés

Passez ces cinq points en revue avant de committer la sortie :

  1. Normalisez les noms de champs : ne laissez pas userId et user_id coexister ; mappez une fois dans une couche de conversion.
  2. Revérifiez le caractère facultatif : comparez avec des réponses réelles et trouvez quels champs peuvent manquer ou être null.
  3. Gardez dates et identifiants en chaînes : documentez l'unité et le format dans le type, et parsez explicitement au point d'utilisation.
  4. Réduisez any et unknown : partout où le générateur a émis any, ajoutez un type ou gardez unknown et ajoutez une garde.
  5. Ajoutez une validation à l'exécution : validez les données externes au moins une fois ; n'attendez pas qu'une interface attrape de mauvaises données.

Si le projet utilise plusieurs bibliothèques JSON (JSON.parse, superjson, lossless-json), leur comportement pour les grands entiers et les dates diffère, donc confirmez-le lors du choix. Voir choix d'une bibliothèque de parsing JSON.

Pourquoi le générateur marque-t-il tous les champs facultatifs comme obligatoires ?

Il a vu un seul échantillon, donc chaque clé apparue est traitée comme toujours présente. Fournissez plusieurs échantillons couvrant des cas différents, ou générez depuis JSON Schema — le tableau required y est la source d'autorité.

Pourquoi déclarer Date dans une interface échoue-t-il à l'exécution ?

JSON.parse ne produit que des chaînes, nombres, booléens, null, objets et tableaux ; il ne construit jamais de Date. Déclarer Date est une promesse de compilation alors que la valeur à l'exécution reste une chaîne. Gardez string dans la couche de transport et convertissez à la frontière.

Les types générés peuvent-ils servir de validation ?

Non. Les types TypeScript sont effacés après compilation et n'imposent rien aux données à l'exécution. Validez les données externes avec zod, valibot, ajv ou une garde de type écrite à la main.

Un identifiant grand entier doit-il être typé number ou string ?

S'il peut dépasser 2^53, il doit être string. JSON.parse perd de la précision au parsing, et le typer number ne fait qu'écrire le bug dans le type où il ne peut plus être corrigé.