JSON Toolkit

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.

Intermedio~9 min di letturaPubblicato il 2026-10-31Aggiornato il 2026-11-04
Copertina della guida a interfacce TypeScript da JSON e type guard

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:

  1. Raccogli diversi esempi reali che coprano sia "chiave mancante" sia "valore null", poi lascia che il generatore inferisca di nuovo.
  2. Dopo la generazione, cambia a mano i campi in ?: T | null e 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[] o any[], 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: interface può essere dichiarata più volte e si fonde automaticamente; type no. È 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: type può esprimere direttamente A | B, Partial<T> e i tipi di template literal; interface no.
  • 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:

  1. Normalizza i nomi dei campi: non lasciare che userId e user_id convivano; mappa una volta in uno strato di conversione.
  2. Ricontrolla l'opzionalità: confronta con le risposte reali e scopri quali campi possono mancare o essere null.
  3. Mantieni date e id come stringhe: documenta unità e formato nel tipo, e analizza esplicitamente al punto di utilizzo.
  4. Riduci any e unknown: ovunque il generatore abbia emesso any, aggiungi un tipo oppure mantieni unknown e aggiungi un guard.
  5. 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.