JSON ke TypeScript: Hasilkan Tipe dari Respons API
Jika Anda pernah memanggil REST API di TypeScript dan mengetik responsnya sebagai any, Anda pasti tahu rasa sakit yang mengikutinya. Autocomplete menghilang. Kesalahan ketik lolos begitu saja. Field yang Anda kira bertipe string ternyata null, dan aplikasi Anda crash di production.
Solusinya? Konversi respons JSON Anda menjadi tipe TypeScript yang tepat. Dalam panduan ini, kita akan membahas pemetaan manual, alat otomatis, dan praktik terbaik yang menjaga integrasi API Anda tetap kokoh.
Mengapa Tipe TypeScript dari JSON Itu Penting
Sistem tipe TypeScript ada untuk menangkap kesalahan sebelum sampai ke pengguna Anda. Ketika Anda memodelkan respons API dengan tipe yang akurat, tiga hal terjadi:
- Autocomplete bekerja di mana-mana. Editor Anda mengetahui bentuk datanya, sehingga Anda berhenti menebak-nebak nama field.
- Refactoring menjadi aman. Ubah nama properti dan compiler akan memberitahu Anda setiap tempat yang rusak.
- Error runtime berkurang. Anda menangani
null,undefined, dan bentuk data tak terduga di batas masuk, bukan jauh di dalam aplikasi Anda.
Tanpa tipe, Anda pada dasarnya menulis JavaScript dengan langkah tambahan. Dengan tipe, Anda mendapatkan kontrak hidup antara kode Anda dan data yang dikonsumsinya.
Pemetaan Manual JSON ke TypeScript
Mari kita mulai dengan dasar-dasarnya. Diberikan sebuah objek JSON, bagaimana cara mengubahnya menjadi interface TypeScript?
Objek Dasar
Berikut adalah respons user sederhana dari sebuah API:
{
"id": 42,
"username": "jdoe",
"email": "jdoe@example.com",
"isActive": true
}
Interface TypeScript yang sesuai:
interface User {
id: number;
username: string;
email: string;
isActive: boolean;
}
Setiap nilai JSON dipetakan ke primitif TypeScript: number, string, atau boolean. Key objek menjadi nama properti.
Objek Bersarang
API di dunia nyata jarang mengembalikan struktur datar. Pertimbangkan user dengan alamat:
{
"id": 42,
"username": "jdoe",
"address": {
"street": "123 Main St",
"city": "Springfield",
"zipCode": "62704",
"country": "US"
}
}
Pisahkan objek bersarang menjadi interface tersendiri:
interface Address {
street: string;
city: string;
zipCode: string;
country: string;
}
interface User {
id: number;
username: string;
address: Address;
}
Ini menjaga setiap tipe tetap fokus dan dapat digunakan ulang. Jika endpoint lain juga mengembalikan Address, Anda menggunakan interface yang sama.
Array
Ketika sebuah field berisi array, Anda menganotasi tipe elemennya:
{
"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[];
}
Gunakan sintaks Type[] untuk kasus sederhana. Untuk generic yang kompleks, Array<Type> bisa lebih jelas, tetapi konsistensi lebih penting daripada pilihan mana yang Anda ambil.
Field Opsional
API sering mengembalikan field yang mungkin ada atau mungkin tidak ada. Tandai dengan ?:
// Terkadang API mengembalikan bio, terkadang tidak
{
"id": 42,
"username": "jdoe",
"bio": "Software developer"
}
interface User {
id: number;
username: string;
bio?: string; // May be absent from the response
}
Field opsional (bio?: string) berarti key-nya mungkin tidak ada sama sekali. Ini berbeda dari field nullable, di mana key-nya ada tetapi nilainya mungkin null.
Menangani Kasus Khusus
API di production itu berantakan. Berikut cara menangani masalah umum yang sering terjadi.
Field Nullable
Ketika sebuah field bisa secara eksplisit bernilai null:
{
"id": 42,
"username": "jdoe",
"avatarUrl": null,
"deletedAt": null
}
interface User {
id: number;
username: string;
avatarUrl: string | null;
deletedAt: string | null; // ISO date string or null
}
Jika sebuah field bisa tidak ada sekaligus null, gabungkan keduanya:
interface User {
id: number;
username: string;
middleName?: string | null; // May be missing, or present but null
}
Union Type
Beberapa field menerima beberapa tipe. API pencarian mungkin mengembalikan hasil campuran:
{
"results": [
{ "type": "user", "id": 1, "username": "jdoe" },
{ "type": "post", "id": 2, "title": "Hello" }
]
}
Modelkan ini dengan discriminated union:
interface UserResult {
type: 'user';
id: number;
username: string;
}
interface PostResult {
type: 'post';
id: number;
title: string;
}
type SearchResult = UserResult | PostResult;
interface SearchResponse {
results: SearchResult[];
}
Field type berfungsi sebagai diskriminan. TypeScript mempersempit tipe secara otomatis saat Anda memeriksanya:
function renderResult(result: SearchResult) {
if (result.type === 'user') {
console.log(result.username); // TypeScript knows this is UserResult
} else {
console.log(result.title); // TypeScript knows this is PostResult
}
}
Enum dari Nilai String
Ketika API mengembalikan sekumpulan nilai string yang tetap, gunakan string literal union alih-alih enum TypeScript:
{ "status": "active" }
// Possible values: "active", "inactive", "suspended"
type UserStatus = 'active' | 'inactive' | 'suspended';
interface User {
id: number;
status: UserStatus;
}
String literal union lebih sederhana, tree-shakeable, dan tidak menghasilkan JavaScript runtime seperti enum. Simpan enum untuk kasus di mana Anda membutuhkan reverse mapping (angka ke nama).
Interface vs Type Alias
Baik interface maupun type bisa mendeskripsikan bentuk objek. Berikut kapan memilih masing-masing:
Gunakan interface untuk bentuk objek yang mungkin Anda perluas:
interface BaseEntity {
id: number;
createdAt: string;
updatedAt: string;
}
interface User extends BaseEntity {
username: string;
email: string;
}
Gunakan type untuk union, intersection, dan tipe komputasi:
type ApiResponse<T> = {
data: T;
meta: { total: number; page: number };
};
type UserOrAdmin = User | Admin;
Dalam praktiknya, keduanya bisa digunakan untuk sebagian besar kasus. Pilih satu konvensi untuk proyek Anda dan patuhi itu. Banyak tim menggunakan interface untuk model data dan type untuk yang lainnya.
Menggunakan Alat Otomatis
Mengetik interface secara manual cocok untuk API kecil, tetapi menjadi membosankan ketika Anda berhadapan dengan endpoint yang mengembalikan 50+ field atau struktur yang sangat bersarang.
Sebelum mengonversi JSON ke tipe, pastikan JSON Anda valid dan terformat dengan baik. JSON formatter kami membersihkan respons API yang berantakan, dan JSON validator menangkap error sintaks sebelum membuang waktu Anda.
Konverter Online
Alat seperti quicktype, json2ts, dan konverter JSON-to-TypeScript memungkinkan Anda menempelkan payload JSON dan mendapatkan interface secara instan. Mereka menangani objek bersarang, array, dan field opsional secara otomatis.
Alur kerja umumnya:
- Panggil API Anda dan salin responsnya
- Tempelkan JSON ke dalam konverter
- Tinjau tipe yang dihasilkan dan sesuaikan penamaannya
- Salin TypeScript ke dalam codebase Anda
Generasi Kode dari Spesifikasi OpenAPI
Jika API Anda memiliki spesifikasi OpenAPI (Swagger), lewati konversi JSON sepenuhnya. Alat seperti openapi-typescript menghasilkan tipe langsung dari spesifikasi:
npx openapi-typescript https://api.example.com/openapi.json -o ./src/types/api.ts
Pendekatan ini lebih unggul karena spesifikasi mendefinisikan field opsional, tipe nullable, dan enum secara eksplisit. Anda mendapatkan tipe yang akurat tanpa menebak dari data sampel.
Validasi Runtime dengan Zod
Untuk keamanan maksimal, definisikan bentuk data Anda dengan Zod dan inferensikan tipe TypeScript darinya:
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>;
// Now validate at runtime
const response = await fetch('/api/users/42');
const data = await response.json();
const user = UserSchema.parse(data); // Throws if shape doesn't match
Dengan Zod, tipe dan logika validasi Anda berada di satu tempat. Jika API mengubah bentuk datanya, parse() langsung melempar error alih-alih membiarkan data buruk menyebar ke seluruh aplikasi Anda.
Mengintegrasikan Tipe dengan Panggilan API
Tipe hanya berguna jika Anda menerapkannya di batas tempat data memasuki aplikasi Anda.
Wrapper Fetch yang Bertipe
Buat fungsi fetch generik yang menerapkan tipe:
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;
}
// Usage
const user = await fetchJson<User>('/api/users/42');
const validatedUser = await fetchJson('/api/users/42', UserSchema);
Axios dengan Tipe
Jika Anda menggunakan Axios, terapkan tipe pada generic respons:
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; // Fully typed
}
React Query / TanStack Query
Di aplikasi React, pasangkan tipe dengan library data-fetching Anda:
import { useQuery } from '@tanstack/react-query';
function useUser(id: number) {
return useQuery({
queryKey: ['user', id],
queryFn: () => fetchJson<User>(`/api/users/${id}`),
});
}
// In your component
const { data: user, isLoading } = useUser(42);
// user is typed as User | undefined
Praktik Terbaik
Aktifkan Strict Null Checks
Di tsconfig.json Anda, aktifkan mode strict (atau minimal strictNullChecks). Ini memaksa Anda menangani null dan undefined secara eksplisit:
{
"compilerOptions": {
"strict": true
}
}
Tanpa strict null checks, TypeScript membiarkan Anda mengakses .username pada nilai yang berpotensi null tanpa peringatan. Itu mengalahkan tujuan dari mengetik respons API Anda.
Gunakan readonly untuk Data Immutable
Respons API adalah data yang Anda terima, bukan data yang seharusnya Anda ubah. Tandai properti sebagai readonly:
interface User {
readonly id: number;
readonly username: string;
readonly email: string;
readonly createdAt: string;
}
Atau gunakan utility type untuk seluruh interface:
type ImmutableUser = Readonly<User>;
Ini mencegah mutasi yang tidak disengaja yang menyebabkan bug halus.
Branded Type untuk ID
Cegah tercampurnya ID dari entitas yang berbeda dengan branded type:
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); // Type error! Can't pass PostId where UserId is expected
Pusatkan Tipe Anda
Simpan semua tipe terkait API dalam direktori khusus:
src/
types/
api/
user.ts
post.ts
common.ts // Shared types like PaginatedResponse
index.ts // Re-exports
Ini membuat tipe mudah ditemukan, diimpor, dan dipelihara. Ketika API berubah, Anda memperbarui satu file dan compiler menunjukkan semua yang rusak.
Validasi di Batas Masuk
Jangan pernah mempercayai data eksternal. Bahkan dengan tipe yang sempurna, API mungkin mengembalikan sesuatu yang tidak terduga. Validasi respons di tempat data memasuki aplikasi Anda:
// Don't do this
const user = (await response.json()) as User; // Trusts blindly
// Do this
const data = await response.json();
const user = UserSchema.parse(data); // Validates shape
Type assertion (as User) membungkam compiler tetapi tidak memeriksa datanya. Validasi runtime (Zod, io-ts, Valibot) benar-benar memverifikasi bahwa bentuknya cocok.
Bekerja dengan Struktur JSON yang Kompleks
Ketika berhadapan dengan payload JSON yang besar atau kompleks, gunakan JSON editor untuk menjelajahi dan memahami strukturnya sebelum menulis tipe. Jauh lebih mudah melihat relasi bersarang di editor yang tepat daripada di teks mentah.
Untuk tim yang bekerja dengan JSON API, lihat panduan terkait berikut:
- JSON Formatting Best Practices — jaga JSON Anda tetap bersih dan mudah dibaca
- JSON Schema Validation Guide — validasi kontrak API dengan JSON Schema
- JSON API Design Patterns — desain API yang mudah di-type
- How to Validate JSON — tangkap payload yang tidak valid sedini mungkin
Menyatukan Semuanya
Berikut contoh lengkap yang mengikat semuanya—dari JSON mentah hingga data yang tervalidasi dan bertipe di komponen React:
import { z } from 'zod';
// 1. Define the schema (single source of truth)
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. Infer the type from the schema
type User = z.infer<typeof UserSchema>;
// 3. Create a typed fetch function
async function getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`Failed to fetch user ${id}`);
const data = await response.json();
return UserSchema.parse(data);
}
// 4. Use in your application with full type safety
const user = await getUser(42);
console.log(user.profile.bio); // string | null — compiler knows
console.log(user.profile.socialLinks); // array or undefined — compiler knows
Tidak ada any. Tidak ada tebakan. Tidak ada kejutan saat runtime.
Kesimpulan
Mengonversi JSON ke tipe TypeScript bukan hanya tentang membuat compiler senang. Ini tentang membangun jaring pengaman yang menangkap kesalahan lebih awal, mendukung autocomplete editor Anda, dan membuat codebase Anda lebih mudah dipelihara.
Mulai dari yang sederhana: salin respons JSON, tulis interface secara manual, dan ketik panggilan fetch Anda. Seiring pertumbuhan proyek Anda, adopsi Zod untuk validasi runtime dan OpenAPI codegen untuk API yang besar. Investasi awal ini akan terbayar saat pertama kali sebuah type error menangkap bug yang seharusnya sampai ke production.