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.

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 :
- Collectez plusieurs échantillons réels couvrant à la fois « clé absente » et « valeur null », puis laissez le générateur réinférer.
- Après génération, modifiez les champs à la main en
?: T | nullet 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[]ouany[], 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 :
interfacepeut être déclaré plusieurs fois et fusionne automatiquement ;typene 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 :
typepeut exprimer directementA | B,Partial<T>et les types de littéraux de gabarit ;interfacene 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 :
- Normalisez les noms de champs : ne laissez pas
userIdetuser_idcoexister ; mappez une fois dans une couche de conversion. - Revérifiez le caractère facultatif : comparez avec des réponses réelles et trouvez quels champs peuvent manquer ou être
null. - Gardez dates et identifiants en chaînes : documentez l'unité et le format dans le type, et parsez explicitement au point d'utilisation.
- Réduisez
anyetunknown: partout où le générateur a émisany, ajoutez un type ou gardezunknownet ajoutez une garde. - 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é.