Le convertisseur crée une ébauche à partir des valeurs fournies. Les ajustements manuels ci-dessous exigent de connaître le contrat de votre API ; les types déduits ne valident pas les données à l’exécution.
Commençons par les fondamentaux. Étant donné un objet JSON, comment le transformer en interface TypeScript ?
Voici une réponse utilisateur simple provenant d'une API :
{
"id": 42,
"username": "jdoe",
"email": "[email protected]",
"isActive": true
}
L'interface TypeScript correspondante :
interface User {
id: number;
username: string;
email: string;
isActive: boolean;
}
Chaque valeur JSON correspond à un type primitif TypeScript : number, string ou boolean. Les clés d'objet deviennent des noms de propriétés.
Les API réelles retournent rarement des structures plates. Considérons un utilisateur avec une adresse :
{
"id": 42,
"username": "jdoe",
"address": {
"street": "123 Main St",
"city": "Springfield",
"zipCode": "62704",
"country": "US"
}
}
Séparez les objets imbriqués dans leurs propres interfaces :
interface Address {
street: string;
city: string;
zipCode: string;
country: string;
}
interface User {
id: number;
username: string;
address: Address;
}
Cela garde chaque type ciblé et réutilisable. Si un autre endpoint retourne également une Address, vous utilisez la même interface.
Lorsqu'un champ contient un tableau, vous annotez le type de l'élément :
{
"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[];
}
Utilisez la syntaxe Type[] pour les cas simples. Pour les génériques complexes, Array<Type> peut être plus clair, mais la cohérence importe plus que le choix.
Les API retournent souvent des champs qui peuvent être présents ou non. Marquez-les avec ? :
{
"id": 42,
"username": "jdoe",
"bio": "Software developer"
}
interface User {
id: number;
username: string;
bio?: string; // Peut être absent de la réponse
}
Un champ optionnel (bio?: string) signifie que la clé peut être entièrement absente. C'est différent d'un champ nullable, où la clé existe mais la valeur peut être null.
Les API en production sont désordonnées. Voici comment gérer les pièges courants.
Lorsqu'un champ peut être explicitement null :
{
"id": 42,
"username": "jdoe",
"avatarUrl": null,
"deletedAt": null
}
interface User {
id: number;
username: string;
avatarUrl: string | null;
deletedAt: string | null; // Chaîne de date ISO ou null
}
Si un champ peut être à la fois absent et null, combinez les deux :
interface User {
id: number;
username: string;
middleName?: string | null; // Peut être absent, ou présent mais null
}
Certains champs acceptent plusieurs types. Une API de recherche peut retourner des résultats mixtes :
{
"results": [
{ "type": "user", "id": 1, "username": "jdoe" },
{ "type": "post", "id": 2, "title": "Hello" }
]
}
Modélisez cela avec une union discriminée :
interface UserResult {
type: 'user';
id: number;
username: string;
}
interface PostResult {
type: 'post';
id: number;
title: string;
}
type SearchResult = UserResult | PostResult;
interface SearchResponse {
results: SearchResult[];
}
Le champ type agit comme discriminant. TypeScript affine le type automatiquement lorsque vous le vérifiez :
function renderResult(result: SearchResult) {
if (result.type === 'user') {
console.log(result.username); // TypeScript sait que c'est UserResult
} else {
console.log(result.title); // TypeScript sait que c'est PostResult
}
}
Lorsqu'une API retourne un ensemble fixe de valeurs string, utilisez des unions de littéraux string plutôt que des enums TypeScript :
{ "status": "active" }
type UserStatus = 'active' | 'inactive' | 'suspended';
interface User {
id: number;
status: UserStatus;
}
Les unions de littéraux string sont plus simples, optimisables par le tree-shaking, et ne génèrent pas de JavaScript à l'exécution contrairement à enum. Réservez enum aux cas où vous avez besoin du mapping inverse (nombre vers nom).
interface et type peuvent tous deux décrire des formes d'objets. Voici quand choisir l'un ou l'autre :
Utilisez interface pour les formes d'objets que vous pourriez étendre :
interface BaseEntity {
id: number;
createdAt: string;
updatedAt: string;
}
interface User extends BaseEntity {
username: string;
email: string;
}
Utilisez type pour les unions, intersections et types calculés :
type ApiResponse<T> = {
data: T;
meta: { total: number; page: number };
};
type UserOrAdmin = User | Admin;
En pratique, les deux fonctionnent dans la plupart des cas. Choisissez une convention pour votre projet et tenez-vous-y. Beaucoup d'équipes utilisent interface pour les modèles de données et type pour tout le reste.
Pour une sécurité maximale, définissez vos formes avec Zod et inférez les types TypeScript à partir d'elles :
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>;
// Maintenant validez à l'exécution
const response = await fetch('/api/users/42');
const data = await response.json();
const user = UserSchema.parse(data); // Lance une erreur si la forme ne correspond pas
Avec Zod, vos types et votre logique de validation cohabitent au même endroit. Si l'API change de forme, parse() lance immédiatement une erreur au lieu de laisser des données incorrectes se propager dans votre application.
Les types ne sont utiles que si vous les appliquez à la frontière où les données entrent dans votre application.
Créez une fonction fetch générique qui impose les types :
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;
}
// Utilisation
const user = await fetchJson<User>('/api/users/42');
const validatedUser = await fetchJson('/api/users/42', UserSchema);
Si vous utilisez Axios, appliquez les types au générique de la réponse :
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; // Entièrement typé
}
Dans les applications React, associez les types à votre bibliothèque de récupération de données :
import { useQuery } from '@tanstack/react-query';
function useUser(id: number) {
return useQuery({
queryKey: ['user', id],
queryFn: () => fetchJson<User>(`/api/users/${id}`),
});
}
// Dans votre composant
const { data: user, isLoading } = useUser(42);
// user est typé comme User | undefined
Dans votre tsconfig.json, activez le mode strict (ou au minimum strictNullChecks). Cela vous oblige à gérer null et undefined explicitement :
{
"compilerOptions": {
"strict": true
}
}
Sans les vérifications strictes de null, TypeScript vous laisse accéder à .username sur une valeur potentiellement null sans se plaindre. Cela annule tout l'intérêt de typer vos réponses API.
readonly pour les données immuablesLes réponses API sont des données que vous avez reçues, pas des données que vous devriez modifier. Marquez les propriétés comme readonly :
interface User {
readonly id: number;
readonly username: string;
readonly email: string;
readonly createdAt: string;
}
Ou utilisez un type utilitaire pour l'interface entière :
type ImmutableUser = Readonly<User>;
Cela empêche les mutations accidentelles qui causent des bugs subtils.
Empêchez la confusion entre les identifiants de différentes entités avec des types marqués :
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); // Erreur de type ! Impossible de passer PostId là où UserId est attendu
Conservez tous les types liés aux API dans un répertoire dédié :
src/
types/
api/
user.ts
post.ts
common.ts // Types partagés comme PaginatedResponse
index.ts // Réexportations
Cela rend les types faciles à trouver, importer et maintenir. Lorsqu'une API change, vous mettez à jour un seul fichier et le compilateur vous montre tout ce qui casse.
Ne faites jamais confiance aux données externes. Même avec des types parfaits, l'API peut retourner quelque chose d'inattendu. Validez les réponses là où elles entrent dans votre application :
// Ne faites pas ça
const user = (await response.json()) as User; // Fait confiance aveuglément
// Faites plutôt ça
const data = await response.json();
const user = UserSchema.parse(data); // Valide la forme
Les assertions de type (as User) font taire le compilateur mais ne vérifient pas les données. La validation à l'exécution (Zod, io-ts, Valibot) vérifie réellement que la forme correspond.
Lorsque vous traitez des payloads JSON volumineux ou complexes, utilisez l'éditeur JSON pour explorer et comprendre la structure avant d'écrire les types. Il est bien plus facile de voir les relations imbriquées dans un éditeur adapté que dans du texte brut.
Pour les équipes travaillant avec des API JSON, consultez ces guides connexes :
Si votre API dispose d'une spécification OpenAPI (Swagger), évitez complètement la conversion JSON. Des outils comme openapi-typescript génèrent les types directement à partir de la spécification :
npx openapi-typescript https://api.example.com/openapi.json -o ./src/types/api.ts
Cette approche est supérieure car la spécification définit explicitement les champs optionnels, les types nullables et les énumérations. Vous obtenez des types précis sans avoir à deviner à partir de données d'exemple.
Le traitement se fait dans votre navigateur. L’enregistrement et le partage sont facultatifs, nécessitent un compte et transmettent le contenu choisi à notre service.
Oui. Le convertisseur génère des interfaces séparées pour chaque objet imbriqué et les référence correctement.
Les types TypeScript générés sont prêts pour la production. Copiez-les ou téléchargez-les en fichier .ts.
Oui. Le convertisseur détecte les types de tableaux et génère la syntaxe de tableau TypeScript correcte (Type[]). Les champs optionnels sont détectés lorsque les valeurs peuvent être null ou undefined.