Da JSON a TypeScript: interfacce, type guard e i limiti della generazione
In sintesi: Generare un'interfaccia da un JSON di esempio è metà del lavoro: campi opzionali, null, unioni, date e interi grandi richiedono una revisione umana, e i tipi non forniscono alcuna validazione a runtime.

In sintesi: genera l'interfaccia per risparmiare digitazione, poi ricontrolla opzionalità e null, mantieni date e interi grandi come stringhe e aggiungi la validazione a runtime al confine; trattare i tipi generati come il contratto non ti dà alcuna validazione.
Un generatore vede solo l'esempio che gli incolli. Se i campi possono mancare, se i valori possono essere null e che formato usano le date sono tutte ipotesi — e le ipotesi sbagliate tocca a te correggerle.
1. Cosa fa davvero il generatore
Incolla un esempio JSON in Da JSON a TypeScript e l'interfaccia che ottieni è un riassunto della forma: conta le chiavi e i tipi di valore che ha visto, poi sceglie la rappresentazione più stretta disponibile.
{
"id": 1,
"email": "ada@example.com",
"profile": { "nickname": "Ada", "age": 36 },
"tags": ["admin", "beta"],
"createdAt": "2026-10-31T08:00:00Z"
}
L'output somiglia approssimativamente a questo:
interface User {
id: number;
email: string;
profile: { nickname: string; age: number };
tags: string[];
createdAt: string;
}
Compila, ma descrive un valore ideale, non il contratto reale dell'endpoint. Il generatore non sa se email può mancare, se tags può essere vuoto o se createdAt porta sempre un fuso orario. Non sa nemmeno che id potrebbe essere una stringa in altri record.
Quindi l'aspettativa corretta è: il generatore risparmia la digitazione, l'essere umano fornisce il significato.
2. Campi opzionali e null: ? non è | null
Questa è la parte dell'output generato che viene copiata più spesso, e la parte che si rompe più spesso.
| Forma TypeScript | Significato | JSON corrispondente |
|---|---|---|
email: string |
Sempre presente, sempre una stringa | {"email":"a@b.c"} |
email?: string |
La chiave può essere assente; quando presente non può essere null | {} oppure {"email":"a@b.c"} |
email: string | null |
La chiave è sempre presente, il valore può essere null | {"email":null} |
email?: string | null |
La chiave può essere assente e il valore può essere null | Tutti e tre i casi |
Il generatore ha visto un solo esempio, quindi di solito emette la prima forma. Se l'API può davvero restituire null, il tipo sta mentendo: tutto compila, e poi email.toLowerCase() genera un errore a runtime.
Ci sono due modi per correggerlo:
- Raccogli diversi esempi reali che coprano sia "chiave mancante" sia "valore null", poi lascia che il generatore inferisca di nuovo.
- Dopo la generazione, cambia a mano i campi in
?: T | nulle gestisci davvero entrambi i rami nel codice.
Non prendere la scorciatoia di marcare tutto ?: string. Opzionale significa "la chiave può non esistere", non "il valore può essere vuoto". undefined e null si comportano anche diversamente in JSON.stringify: il primo viene omesso, il secondo viene conservato come null.
3. Elementi di array non omogenei e tipi unione
Gli array sono la struttura più facile da sbagliare. Se tags compare come ["admin"], [] e ["beta", 3] in tre record, il generatore può emettere string[], (string | number)[] o unknown[], a seconda degli esempi che ha visto.
Deve deciderlo una persona:
- I tipi degli elementi sono davvero misti: scrivi
(string | number)[]e restringi al punto di utilizzo. - Solo un record è sporco: correggi i dati. Non nasconderlo con
any[]. - Array vuoti: il generatore non può inferire un tipo di elemento e ripiega su
never[]oany[], quindi devi completarlo tu. - Tuple a lunghezza fissa: usa
[string, string]solo quando il business garantisce davvero la lunghezza; altrimenti aggiungere un elemento in seguito diventa un errore di compilazione.
Anche gli array di oggetti richiedono attenzione quando le chiavi non sono omogenee:
interface OrderItem {
sku: string;
quantity: number;
discount?: number; // some records do not carry this field
}
Metti l'opzionalità sul tipo dell'elemento, non sull'array. items?: OrderItem[] significa che l'array stesso può essere assente, che è un'affermazione del tutto diversa.
4. Date, interi grandi, enum: tipi che JSON non ha
JSON ha solo stringhe e numeri; il significato di business vive nelle convenzioni. Il generatore inferisce dai letterali, quindi correggi a mano:
| Letterale JSON | Output generato abituale | Cosa dovrebbe essere |
|---|---|---|
"2026-10-31T08:00:00Z" |
string |
Mantieni string nello strato di trasporto e converti al punto di utilizzo; non dichiarare mai Date direttamente |
1717075200000 |
number |
Usa number per un timestamp in millisecondi e documenta l'unità |
"1234567890123456789" |
string |
Mantienilo string così number non può perdere precisione |
1 e "1" misti |
number | string |
Normalizza a una sola forma; non spingere l'unione nel codice di business |
"admin" |
string |
Un'unione di letterali 'admin' | 'editor', oppure una costante enum condivisa |
true e "true" misti |
boolean | string |
Correggi la sorgente dati invece di generare questo tipo |
La regola più importante: le date in JSON sono sempre stringhe. Scrivere createdAt: Date è un comune autoinganno — JSON.parse non costruisce mai un Date, quindi il valore a runtime resta una stringa e .getTime() genera un errore. Mantieni il tipo di trasporto come string, chiama esplicitamente new Date(value) al confine e controlla Number.isNaN.
Gli interi grandi funzionano allo stesso modo. Una volta che un id supera 2^53, JSON.parse ha già perso precisione, e generare number non fa che cementare il bug nel tipo. Il backend restituisce una stringa, il frontend la mantiene stringa; è l'unica opzione affidabile.
5. I tipi generati non forniscono alcuna validazione a runtime
I tipi TypeScript vengono cancellati in fase di compilazione. Per quanto precisa sia l'interfaccia, non può fermare una risposta {"id": "x"} — vincola il tuo codice, non i byte che arrivano dalla rete.
/**
* 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';
}
Un type guard copre solo i campi che scrivi nel controllo. Man mano che i campi si moltiplicano, i guard scritti a mano diventano una seconda definizione da mantenere, che si allontana dall'interfaccia.
Il compromesso pratico è questo:
| Approccio | Validazione a runtime | Origine dei tipi | Si adatta a |
|---|---|---|---|
| Solo interfaccia | Nessuna | Scritti a mano o generati | Dati interni fidati, vincoli in fase di compilazione |
| Interfaccia più guard scritto a mano | Sì | Due definizioni che divergono | Pochi campi, confine chiaro |
| zod / valibot | Sì | Tipi inferiti dallo schema | API esterne, form, configurazione |
| JSON Schema più validatore | Sì | Tipi generati dallo schema | Contratti cross-linguaggio, documentazione |
Se i dati vengono dall'esterno, fai dello schema l'unica fonte di verità e deriva i tipi con z.infer o json-schema-to-ts. Allora "la validazione passa" e "il tipo è corretto" sono lo stesso fatto, e non c'è una seconda definizione che possa divergere. Per l'approccio completo di sincronizzazione bidirezionale, vedi Sincronizzazione tra Schema e tipi TypeScript.
6. Interface o type: non discutere sull'output generato
I generatori di solito emettono interface, ma i due sono intercambiabili nella maggior parte delle situazioni. Ciò che conta davvero sono tre capacità:
- Declaration merging:
interfacepuò essere dichiarata più volte e si fonde automaticamente;typeno. È utile quando si rattoppano tipi di terze parti, ma i modelli di business dovrebbero evitarlo — la fusione lascia che i "campi extra" sfuggano al compilatore senza che nessuno se ne accorga. - Unioni e tipi mappati:
typepuò esprimere direttamenteA | B,Partial<T>e i tipi di template literal;interfaceno. - Ricorsione: entrambi la supportano, ma le forme ricorsive complesse si leggono più naturalmente come
type.
Una regola di squadra più utile: usa interface per le forme degli oggetti, type per le unioni, i tipi di utilità e i tipi di funzione. Lascia l'output generato al suo valore predefinito e investi il tempo di revisione su opzionalità e validazione a runtime.
7. Toolchain: tre modi per generare
| Approccio | Input | Punto di forza | Limitazione |
|---|---|---|---|
| Il Da JSON a TypeScript del sito | JSON di esempio | Funziona subito, elaborato localmente | Basato solo sull'esempio corrente; richiede una restrizione manuale |
| quicktype | JSON / JSON Schema | Output multi-linguaggio, integrazione CLI | Inferenza prudente, le unioni possono essere troppo ampie |
| json-schema-to-typescript | JSON Schema | Guidato dal contratto e riproducibile | Richiede prima uno schema affidabile |
Una tipica invocazione di quicktype:
npx quicktype -s json -o user.ts user.json --just-types
--just-types emette solo i tipi e nessun codice di conversione a runtime, il che si adatta ai progetti che hanno già uno strato di parsing. Se lo schema è scritto a mano o prodotto dal backend, json-schema-to-typescript è più adatto perché rispetta required, enum e additionalProperties — vincoli che l'inferenza sugli esempi semplicemente non può vedere.
La generazione dello schema ha le sue trappole, come marcare ogni campo come obbligatorio o trasformare silenziosamente null in opzionale; vedi trappole comuni dei generatori JSON Schema.
8. Una checklist prima di confermare i tipi generati
Passa in rassegna questi cinque punti prima di confermare l'output:
- Normalizza i nomi dei campi: non lasciare che
userIdeuser_idconvivano; mappa una volta in uno strato di conversione. - Ricontrolla l'opzionalità: confronta con le risposte reali e scopri quali campi possono mancare o essere
null. - Mantieni date e id come stringhe: documenta unità e formato nel tipo, e analizza esplicitamente al punto di utilizzo.
- Riduci
anyeunknown: ovunque il generatore abbia emessoany, aggiungi un tipo oppure mantieniunknowne aggiungi un guard. - Aggiungi la validazione a runtime: valida i dati esterni almeno una volta; non aspettarti che un'interfaccia intercetti dati cattivi.
Se il progetto usa diverse librerie JSON (JSON.parse, superjson, lossless-json), il loro comportamento per interi grandi e date differisce, quindi verificalo in fase di scelta. Vedi Scelta della libreria di parsing JSON.
Perché il generatore marca ogni campo opzionale come obbligatorio?
Ha visto un solo esempio, quindi ogni chiave comparsa viene trattata come sempre presente. Fornisci più esempi che coprano casi diversi, oppure genera da JSON Schema — l'array required è lì la fonte autorevole.
Perché dichiarare Date in un'interfaccia fallisce a runtime?
JSON.parse produce solo stringhe, numeri, booleani, null, oggetti e array; non costruisce mai un Date. Dichiarare Date è una promessa in fase di compilazione mentre il valore a runtime resta una stringa. Mantieni string nello strato di trasporto e converti al confine.
I tipi generati possono essere usati come validazione?
No. I tipi TypeScript vengono cancellati dopo la compilazione e non impongono nulla ai dati a runtime. Valida i dati esterni con zod, valibot, ajv o un type guard scritto a mano.
Un id intero grande deve essere tipizzato come number o string?
Se può superare 2^53, deve essere string. JSON.parse perde precisione durante il parsing, e tipizzarlo come number non fa che iscrivere il bug nel tipo dove non può più essere correggerlo.