
Cách tạo TypeScript Types từ JSON chuẩn xác: Tránh bẫy 'Optional Fields' sai lầm
Bạn đã bao giờ gặp lỗi khi dùng công cụ chuyển đổi JSON sang TypeScript? Bài viết này phân tích 'vấn đề mẫu đơn' (single-sample problem) khiến các trường optional bị thiếu và hướng dẫn cách merge dữ liệu đa mẫu để tạo type an toàn, chính xác tuyệt đối.

Nếu bạn đã từng copy một đối tượng JSON vào một công cụ "JSON to TypeScript" và nhận lại một interface trông có vẻ hoàn hảo, nhưng ngay ngày hôm sau API trả về một cấu trúc dữ liệu hơi khác và làm hỏng toàn bộ bản build của bạn — thì bạn đã gặp phải "vấn đề mẫu đơn" (single-sample problem). Đây là lỗi phổ biến nhất nhưng ít được nhắc đến nhất trong việc chuyển đổi JSON sang TypeScript.
Bản chất của việc "Generate TypeScript Types từ JSON"
Về cốt lõi, việc chuyển đổi JSON sang TypeScript là một bài tập ánh xạ cấu trúc (structural mapping). JSON có một tập hợp nhỏ các kiểu giá trị: string, number, boolean, null, array, object. Mỗi kiểu này đều có một đối tác tương ứng trong TypeScript.
Ví dụ đơn giản:
{
"id": 42,
"name": "Ada Lovelace",
"active": true
}
TypeScript tương ứng:
export interface Root {
id: number;
name: string;
active: boolean;
}
Đối với một đối tượng đơn lẻ, công việc rất đơn giản. Tuy nhiên, sự phức tạp xuất hiện khi dữ liệu của bạn là một mảng các đối tượng — hình thái phổ biến nhất của hầu hết các phản hồi REST API thực tế.
Vấn đề mẫu đơn: Tại sao các trường Optional lại bị bỏ sót?
Hãy tưởng tượng API của bạn trả về danh sách người dùng, và một trong số họ bị thiếu trường email vì họ đăng ký qua SSO:
[
{ "id": 1, "name": "Ada", "email": "[email protected]" },
{ "id": 2, "name": "Alan" }
]
Một công cụ chuyển đổi chỉ nhìn vào phần tử đầu tiên của mảng sẽ tạo ra:
export interface Root {
id: number;
name: string;
email: string;
}
Đây là lỗi nghiêm trọng. Trường email không được đảm bảo luôn tồn tại, nhưng kiểu dữ liệu được tạo ra lại khẳng định nó luôn có. Lỗi này sẽ không xuất hiện ngay lập tức mà sẽ bùng phát dưới dạng undefined tại runtime, dù TypeScript đã cam kết đó là một string.
Giải pháp: Hợp nhất đa mẫu (Multi-sample Merging)
Để khắc phục, chúng ta cần hợp nhất tập hợp các khóa (keys) từ mọi phần tử trong mảng, không chỉ phần tử đầu tiên. Kết quả đúng phải là:
export interface Root {
id: number;
name: string;
email?: string; // Dấu ? cho thấy trường này có thể vắng mặt
}
Xử lý các kiểu dữ liệu khác nhau (Union Types)
Nếu một trường là number trong bản ghi này nhưng là string trong bản ghi khác, một công cụ tốt phải tạo ra kiểu Union:
[{ "value": 42 }, { "value": "forty-two" }]
Kết quả TypeScript chính xác:
export interface Root {
value: number | string;
}
Thiết kế cấu trúc: Inline hay Named Interfaces?
Khi xử lý các đối tượng lồng nhau, bạn có hai lựa chọn:
- Inline: Giữ mọi thứ trong một khối.
export interface Root { id: number; address: { city: string; postcode: string }; } - Extracted (Trích xuất): Đặt tên riêng cho cấu trúc lồng nhau, giúp tái sử dụng và dễ đọc hơn.
export interface Address { city: string; postcode: string; } export interface Root { id: number; address: Address; }
Các tiêu chuẩn cần kiểm tra ở một công cụ tốt
Trước khi tin tưởng vào output của một công cụ, hãy đảm bảo nó xử lý được các trường hợp sau:
- Mảng rỗng: Nên được infer là
unknown[]thay vìany[]. - Khóa không hợp lệ: Các khóa chứa dấu gạch ngang, khoảng trắng hoặc bắt đầu bằng số cần được tự động đặt trong dấu ngoặc kép (ví dụ:
"last-login": string;). - Giá trị null: Phải được xử lý rõ ràng dưới dạng
T | nullhoặckey?: T. - Tùy chọn xuất: Cho phép bật/tắt
exportvàreadonly.
Kết luận
Việc tạo TypeScript types từ JSON là một công việc đòi hỏi sự chính xác cao. Đừng dựa vào các công cụ chỉ nhìn vào phần tử đầu tiên của mảng. Hãy ưu tiên các giải pháp thực hiện đệ quy và hợp nhất dữ liệu từ toàn bộ tập mẫu để đảm bảo code của bạn an toàn và không gặp lỗi runtime bất ngờ.
Do you like this post?
Upvote to push this post higher on the community feed
