Il convertitore genera una bozza dai valori del campione. Gli aggiustamenti manuali seguenti richiedono la conoscenza del contratto API; i tipi dedotti non convalidano i dati durante l’esecuzione.
Iniziamo con i fondamentali. Dato un oggetto JSON, come lo trasformi in un'interfaccia TypeScript?
Ecco una semplice risposta utente da un'API:
{
"id": 42,
"username": "jdoe",
"email": "[email protected]",
"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à.
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.
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.
Le API spesso restituiscono campi che possono o non possono essere presenti. Marcali con ?:
{
"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.
Le API di produzione sono disordinate. Ecco come gestire i problemi comuni.
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
}
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
}
}
Quando un'API restituisce un set fisso di valori stringa, usa unioni di letterali stringa invece di enum TypeScript:
{ "status": "active" }
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).
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.
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.
I tipi sono utili solo se li applichi al confine dove i dati entrano nella tua applicazione.
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);
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
}
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
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.
readonly per dati immutabiliLe 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.
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
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.
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.
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:
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.
L’elaborazione avviene nel browser. Salvataggio e condivisione sono facoltativi, richiedono un account e inviano il contenuto scelto al nostro servizio.
Sì. Il convertitore genera interfacce separate per ogni oggetto annidato e li referenzia correttamente.
I tipi TypeScript generati sono pronti per la produzione. Copiali o scaricali come file .ts.
Sì. Il convertitore rileva i tipi array e genera la corretta sintassi array TypeScript (Type[]). I campi opzionali vengono rilevati quando i valori possono essere null o undefined.