Da JSON a TypeScript: Genera tipi dalle risposte API
Se hai mai chiamato un'API REST in TypeScript e tipizzato la risposta come any, conosci il dolore che ne consegue. L'autocompletamento scompare. Gli errori di battitura si insinuano. Un campo che pensavi fosse una string si rivela null, e la tua app crolla in produzione.
La soluzione? Convertire le tue risposte JSON in tipi TypeScript appropriati. In questa guida, esamineremo la mappatura manuale, gli strumenti automatizzati e le best practice che mantengono le tue integrazioni API solide come una roccia.
Perché i tipi TypeScript da JSON sono importanti
Il sistema di tipi di TypeScript esiste per intercettare errori prima che raggiungano i tuoi utenti. Quando modelli una risposta API con tipi accurati, succedono tre cose:
- L'autocompletamento funziona ovunque. Il tuo editor conosce la forma dei dati, quindi smetti di indovinare i nomi dei campi.
- I refactoring diventano sicuri. Rinomina una proprietà e il compilatore ti dice ogni punto che si rompe.
- Gli errori a runtime diminuiscono. Gestisci
null,undefinede forme inattese al confine invece che nelle profondità della tua app.
Senza tipi, stai essenzialmente scrivendo JavaScript con passaggi extra. Con i tipi, ottieni un contratto vivente tra il tuo codice e i dati che consuma.
Mappatura manuale da JSON a TypeScript
Iniziamo con i fondamentali. Dato un oggetto JSON, come lo trasformi in un'interfaccia TypeScript?
Oggetti base
Ecco una semplice risposta utente da un'API:
{
"id": 42,
"username": "jdoe",
"email": "jdoe@example.com",
"isActive": true
}
L'interfaccia TypeScript corrispondente:
interface User {
id: number;
username: string;
email: string;
isActive: boolean;
}
Ogni valore JSON si mappa a un primitivo TypeScript: number, string o boolean. Le chiavi dell'oggetto diventano nomi di proprietà.
Oggetti annidati
Le API reali raramente restituiscono strutture piatte. Considera un utente con un indirizzo:
{
"id": 42,
"username": "jdoe",
"address": {
"street": "123 Main St",
"city": "Springfield",
"zipCode": "62704",
"country": "US"
}
}
Separa gli oggetti annidati nelle proprie interfacce:
interface Address {
street: string;
city: string;
zipCode: string;
country: string;
}
interface User {
id: number;
username: string;
address: Address;
}
Questo mantiene ogni tipo focalizzato e riutilizzabile. Se un altro endpoint restituisce anche un Address, usi la stessa interfaccia.
Array
Quando un campo contiene un array, annoti il tipo dell'elemento:
{
"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[];
}
Usa la sintassi Type[] per casi semplici. Per generici complessi, Array<Type> può essere più chiaro, ma la coerenza conta più di quale scegli.
Campi opzionali
Le API spesso restituiscono campi che possono o non possono essere presenti. Marcali con ?:
// A volte l'API restituisce bio, a volte no
{
"id": 42,
"username": "jdoe",
"bio": "Software developer"
}
interface User {
id: number;
username: string;
bio?: string; // Potrebbe essere assente dalla risposta
}
Un campo opzionale (bio?: string) significa che la chiave potrebbe mancare completamente. Questo è diverso da un campo nullable, dove la chiave esiste ma il valore potrebbe essere null.
Gestione dei casi limite
Le API di produzione sono disordinate. Ecco come gestire i problemi comuni.
Campi nullable
Quando un campo può essere esplicitamente null:
{
"id": 42,
"username": "jdoe",
"avatarUrl": null,
"deletedAt": null
}
interface User {
id: number;
username: string;
avatarUrl: string | null;
deletedAt: string | null; // Stringa data ISO o null
}
Se un campo può essere sia assente che null, combina i due:
interface User {
id: number;
username: string;
middleName?: string | null; // Può mancare, o essere presente ma null
}
Tipi union
Alcuni campi accettano tipi multipli. Un'API di ricerca potrebbe restituire risultati misti:
{
"results": [
{ "type": "user", "id": 1, "username": "jdoe" },
{ "type": "post", "id": 2, "title": "Hello" }
]
}
Modella questo con un'unione discriminata:
interface UserResult {
type: 'user';
id: number;
username: string;
}
interface PostResult {
type: 'post';
id: number;
title: string;
}
type SearchResult = UserResult | PostResult;
interface SearchResponse {
results: SearchResult[];
}
Il campo type funge da discriminante. TypeScript restringe il tipo automaticamente quando lo verifichi:
function renderResult(result: SearchResult) {
if (result.type === 'user') {
console.log(result.username); // TypeScript sa che questo è UserResult
} else {
console.log(result.title); // TypeScript sa che questo è PostResult
}
}
Enum da valori stringa
Quando un'API restituisce un set fisso di valori stringa, usa unioni di letterali stringa invece di enum TypeScript:
{ "status": "active" }
// Valori possibili: "active", "inactive", "suspended"
type UserStatus = 'active' | 'inactive' | 'suspended';
interface User {
id: number;
status: UserStatus;
}
Le unioni di letterali stringa sono più semplici, eliminabili dal tree-shaking e non generano JavaScript a runtime come enum. Riserva enum per i casi in cui hai bisogno di mappatura inversa (numero a nome).
Interfacce vs alias di tipo
Sia interface che type possono descrivere forme di oggetti. Ecco quando scegliere ciascuno:
Usa interface per forme di oggetti che potresti estendere:
interface BaseEntity {
id: number;
createdAt: string;
updatedAt: string;
}
interface User extends BaseEntity {
username: string;
email: string;
}
Usa type per unioni, intersezioni e tipi calcolati:
type ApiResponse<T> = {
data: T;
meta: { total: number; page: number };
};
type UserOrAdmin = User | Admin;
In pratica, entrambi funzionano per la maggior parte dei casi. Scegli una convenzione per il tuo progetto e attieniti ad essa. Molti team usano interface per i modelli dati e type per tutto il resto.
Utilizzo di strumenti automatizzati
Tipizzare le interfacce manualmente funziona per API piccole, ma diventa tedioso quando si ha a che fare con endpoint che restituiscono più di 50 campi o strutture profondamente annidate.
Prima di convertire JSON in tipi, assicurati che il tuo JSON sia valido e ben formattato. Il nostro formattatore JSON pulisce le risposte API disordinate, e il validatore JSON intercetta errori di sintassi prima che ti facciano perdere tempo.
Convertitori online
Strumenti come quicktype, json2ts e convertitori JSON-to-TypeScript ti permettono di incollare un payload JSON e ottenere interfacce istantaneamente. Gestiscono oggetti annidati, array e campi opzionali automaticamente.
Il flusso di lavoro tipico:
- Chiama la tua API e copia la risposta
- Incolla il JSON nel convertitore
- Rivedi i tipi generati e aggiusta i nomi
- Copia il TypeScript nella tua codebase
Generazione di codice da specifiche OpenAPI
Se la tua API ha una specifica OpenAPI (Swagger), salta completamente la conversione JSON. Strumenti come openapi-typescript generano tipi direttamente dalla specifica:
npx openapi-typescript https://api.example.com/openapi.json -o ./src/types/api.ts
Questo approccio è superiore perché la specifica definisce campi opzionali, tipi nullable ed enum esplicitamente. Ottieni tipi accurati senza indovinare dai dati di esempio.
Validazione a runtime con Zod
Per la massima sicurezza, definisci le tue forme con Zod e inferisci i tipi TypeScript da esse:
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>;
// Ora valida a runtime
const response = await fetch('/api/users/42');
const data = await response.json();
const user = UserSchema.parse(data); // Lancia errore se la forma non corrisponde
Con Zod, i tuoi tipi e la logica di validazione vivono in un unico posto. Se l'API cambia forma, parse() lancia un errore immediatamente invece di lasciare che dati errati si propaghino nella tua app.
Integrazione dei tipi con le chiamate API
I tipi sono utili solo se li applichi al confine dove i dati entrano nella tua applicazione.
Wrapper fetch tipizzato
Crea una funzione fetch generica che impone i tipi:
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;
}
// Utilizzo
const user = await fetchJson<User>('/api/users/42');
const validatedUser = await fetchJson('/api/users/42', UserSchema);
Axios con tipi
Se usi Axios, applica i tipi al generico della risposta:
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; // Completamente tipizzato
}
React Query / TanStack Query
Nelle app React, abbina i tipi alla tua libreria di data fetching:
import { useQuery } from '@tanstack/react-query';
function useUser(id: number) {
return useQuery({
queryKey: ['user', id],
queryFn: () => fetchJson<User>(`/api/users/${id}`),
});
}
// Nel tuo componente
const { data: user, isLoading } = useUser(42);
// user è tipizzato come User | undefined
Best practice
Abilita i controlli strict null
Nel tuo tsconfig.json, abilita la modalità strict (o come minimo strictNullChecks):
{
"compilerOptions": {
"strict": true
}
}
Senza controlli strict null, TypeScript ti permette di accedere a .username su un valore potenzialmente null senza lamentarsi. Questo vanifica lo scopo di tipizzare le tue risposte API.
Usa readonly per dati immutabili
Le risposte API sono dati che hai ricevuto, non dati che dovresti mutare. Marca le proprietà come readonly:
interface User {
readonly id: number;
readonly username: string;
readonly email: string;
readonly createdAt: string;
}
O usa un tipo utility per l'intera interfaccia:
type ImmutableUser = Readonly<User>;
Questo previene mutazioni accidentali che causano bug sottili.
Tipi branded per gli ID
Previeni la confusione tra ID di entità diverse con tipi branded:
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); // Errore di tipo! Non puoi passare PostId dove è atteso UserId
Centralizza i tuoi tipi
Mantieni tutti i tipi relativi all'API in una directory dedicata:
src/
types/
api/
user.ts
post.ts
common.ts // Tipi condivisi come PaginatedResponse
index.ts // Re-export
Questo rende i tipi facili da trovare, importare e mantenere. Quando un'API cambia, aggiorni un file e il compilatore ti mostra tutto ciò che si rompe.
Valida al confine
Non fidarti mai dei dati esterni. Anche con tipi perfetti, l'API potrebbe restituire qualcosa di inatteso. Valida le risposte dove entrano nella tua app:
// Non fare così
const user = (await response.json()) as User; // Si fida ciecamente
// Fai così
const data = await response.json();
const user = UserSchema.parse(data); // Valida la forma
Le asserzioni di tipo (as User) silenziano il compilatore ma non verificano i dati. La validazione a runtime (Zod, io-ts, Valibot) verifica effettivamente che la forma corrisponda.
Lavorare con strutture JSON complesse
Quando si ha a che fare con payload JSON grandi o complessi, usa l'editor JSON per esplorare e comprendere la struttura prima di scrivere i tipi. È molto più facile vedere le relazioni annidate in un editor appropriato che in testo grezzo.
Per i team che lavorano con API JSON, consulta queste guide correlate:
- Best practice per la formattazione JSON — mantieni il tuo JSON pulito e leggibile
- Guida alla validazione JSON Schema — valida i contratti API con JSON Schema
- Pattern di design per API JSON — progetta API facili da tipizzare
- Come validare JSON — intercetta payload malformati in anticipo
Mettere tutto insieme
Ecco un esempio completo che lega tutto insieme — dal JSON grezzo ai dati validati e tipizzati in un componente React:
import { z } from 'zod';
// 1. Definisci lo schema (unica fonte di verità)
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. Inferisci il tipo dallo schema
type User = z.infer<typeof UserSchema>;
// 3. Crea una funzione fetch tipizzata
async function getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`Impossibile recuperare l'utente ${id}`);
const data = await response.json();
return UserSchema.parse(data);
}
// 4. Usa nella tua applicazione con piena sicurezza dei tipi
const user = await getUser(42);
console.log(user.profile.bio); // string | null — il compilatore lo sa
console.log(user.profile.socialLinks); // array o undefined — il compilatore lo sa
Nessun any. Nessuna indovinazione. Nessuna sorpresa a runtime.
Conclusione
Convertire JSON in tipi TypeScript non significa solo rendere felice il compilatore. Si tratta di costruire una rete di sicurezza che intercetta errori in anticipo, potenzia l'autocompletamento del tuo editor e rende la tua codebase più facile da mantenere.
Inizia in modo semplice: copia una risposta JSON, scrivi l'interfaccia manualmente e tipizza le tue chiamate fetch. Man mano che il tuo progetto cresce, adotta Zod per la validazione a runtime e la generazione di codice OpenAPI per API grandi. L'investimento iniziale si ripaga da solo la prima volta che un errore di tipo intercetta un bug che avrebbe raggiunto la produzione.