Konverter membuat draf dari nilai sampel. Penyesuaian manual berikut memerlukan pengetahuan tentang kontrak API; tipe hasil inferensi tidak memvalidasi data saat dijalankan.
Mari kita mulai dengan dasar-dasarnya. Diberikan sebuah objek JSON, bagaimana cara mengubahnya menjadi interface TypeScript?
Berikut adalah respons user sederhana dari sebuah API:
{
"id": 42,
"username": "jdoe",
"email": "[email protected]",
"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.
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.
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.
API sering mengembalikan field yang mungkin ada atau mungkin tidak ada. Tandai dengan ?:
{
"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.
API di production itu berantakan. Berikut cara menangani masalah umum yang sering terjadi.
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
}
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
}
}
Ketika API mengembalikan sekumpulan nilai string yang tetap, gunakan string literal union alih-alih enum TypeScript:
{ "status": "active" }
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).
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.
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.
Tipe hanya berguna jika Anda menerapkannya di batas tempat data memasuki aplikasi Anda.
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);
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
}
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
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.
readonly untuk Data ImmutableRespons 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.
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
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.
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.
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:
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.
Pemrosesan berjalan di browser. Penyimpanan dan berbagi bersifat opsional, memerlukan akun, dan mengirim konten pilihan Anda ke layanan kami.
Ya. Konverter menghasilkan interface terpisah untuk setiap objek bersarang dan mereferensikannya dengan benar.
Tipe TypeScript yang dihasilkan siap untuk produksi. Salin atau unduh sebagai file .ts.
Ya. Konverter mendeteksi tipe array dan menghasilkan sintaks array TypeScript yang benar (Type[]). Field opsional terdeteksi ketika nilai bisa berupa null atau undefined.