Viết tay interface cho một API JSON vừa chậm vừa dễ sai, nên phần lớn nhóm phát triển sinh bản nháp đầu tiên từ một phản hồi mẫu. Bản nháp đó hữu ích, nhưng nó chỉ là phỏng đoán dựa trên một ví dụ, và kiểu chỉ có tác dụng khi dữ liệu đến lúc chạy thật sự khớp với nó. Bài này sinh kiểu từ một phản hồi đơn hàng giống thật, chỉ ra công cụ sinh làm đúng điều gì và không thể biết điều gì, rồi xử lý vấn đề âm thầm hơn: những chỗ any lọt trở lại vào một mã nguồn trông như đã có kiểu đầy đủ. Mọi đầu ra dưới đây đều lấy từ công cụ JSON sang TypeScript.
Mẫu dữ liệu
Đây là một đơn hàng như API có thể trả về, chép từ tab Network của trình duyệt:
{
"id": 1042,
"status": "shipped",
"total": 259.9,
"paid_at": null,
"customer": { "id": 7, "name": "Trần Minh An", "email": "[email protected]" },
"items": [
{ "sku": "MUG-01", "qty": 2, "price": 89.95 },
{ "sku": "TEE-L", "qty": 1, "price": 80, "gift_note": "Happy birthday" }
],
"tags": ["priority", "gift"],
"shipping-address": { "city": "Đà Nẵng", "zip": null }
}Công cụ sinh ra gì
export interface Root {
id: number;
status: string;
total: number;
paid_at: null;
customer: Customer;
items: Item[];
tags: string[];
"shipping-address": ShippingAddress;
}
export interface Item {
sku: string;
qty: number;
price: number;
gift_note?: string;
}(Customer và ShippingAddress theo cùng mẫu.) Có ba quyết định đáng chú ý:
- Hai dòng hàng được gộp lại. Chỉ dòng thứ hai có
gift_note, nên trường này thành tùy chọn. Một dòng có giá89.95, dòng kia80; cả hai đều lànumber, còn trong đầu ra Go trường này làfloat64chứ không phảiint64vì có một mẫu mang số thập phân. - Khóa có dấu gạch ngang được đặt trong nháy kép.
"shipping-address"là tên thuộc tính hợp lệ, chỉ là không viết được nếu thiếu nháy, và bạn đọc nó bằngorder["shipping-address"]. - `paid_at` có kiểu `null`. Trông thì sai nhưng đó là câu trả lời trung thực duy nhất: mọi mẫu đều có null ở đó. Công cụ không thể biết rằng đơn đã thanh toán sẽ mang một mốc thời gian.
Bổ sung những gì một mẫu không thể biết
Một mẫu chỉ cho thấy một phản hồi. Trước khi commit, hãy sửa kiểu theo hiểu biết của bạn về API:
- Trường bị null trong mẫu.
paid_at: nullphải làpaid_at: string | null. Cách nhanh nhất để có điều này tự động là dán hai đơn hàng trong một mảng, một đơn chưa trả và một đơn đã trả; công cụ gộp chúng thànhstring | null. - Chuỗi có tập giá trị cố định.
status: stringchấp nhận cả"shiped". Hãy thay bằngstatus: "pending" | "paid" | "shipped" | "cancelled", trình biên dịch sẽ bắt lỗi gõ nhầm và chỉ ra nhánh nào một câuswitchcòn thiếu. - Ngày tháng. JSON không có kiểu ngày, nên mốc thời gian đến dưới dạng chuỗi. Giữ chúng là
stringtrong kiểu truyền tải và đổi sangDateở một chỗ duy nhất, đừng giả vờ API gửi đối tượngDate. - Mã định danh.
id: numberổn cho đến khi ID vượt 2^53 − 1 (9.007.199.254.740.991), điều mà một số cơ sở dữ liệu dùng khóa 64 bit có thể chạm tới. Những ID đó phải được gửi dưới dạng chuỗi, vì số của JavaScript không giữ chính xác được. - Tên.
Rootnên làOrder. Đặt tên kiểu gốc trong công cụ trước khi sinh để các tên lồng bên trong cũng dễ đọc.
Vì sao any vẫn quay lại
Sau tất cả công sức đó, người ta hay phát hiện các kiểu chẳng bảo vệ được gì, vì giá trị chảy vào chúng chưa bao giờ được kiểm tra. Đây là những chỗ rò thường gặp:
- `response.json()` trả về `Promise<any>`. Định nghĩa kiểu của DOM khai báo như vậy. Nên
const order: Order = await res.json()vẫn biên dịch dù máy chủ gửi gì; chú thích kiểu ở đây là một lời khẳng định, không phải phép kiểm tra. - `JSON.parse` trả về `any` vì cùng lý do, và việc đọc từ
localStoragequa nó cũng vậy. - Ép kiểu bằng `as`.
data as Orderlàm trình biên dịch im lặng mà không nhìn vàodata. - Kiểu tiện ích lỏng lẻo. Một
Record<string, any>dùng chung hay một hàm kiểu(x: any) => voidlâyanysang mọi thứ nó chạm vào, và truy cập thuộc tính củaanylại cho raany. - Thư viện không có kiểu. Thư viện thiếu kiểu, hoặc có
anytrong chữ ký công khai, đưa thẳng nó vào mã của bạn.
Cách sửa là coi mạng là nguồn không đáng tin. Gán kiểu unknown cho giá trị thô, buộc phải kiểm tra trước khi dùng, và xác thực một lần ở ranh giới. Đầu ra Zod của công cụ làm đúng việc đó:
const order = OrderSchema.parse(await res.json()); // ném lỗi nếu sai hình dạng
// từ đây order có kiểu đầy đủ, và kiểu với phép kiểm tra không thể lệch nhauSau đó để trình biên dịch và linter giúp sức: chế độ strict (gồm noImplicitAny và, từ TypeScript 4.4, useUnknownInCatchVariables), cùng các quy tắc của typescript-eslint là no-explicit-any và họ no-unsafe-*, vốn cảnh báo khi một giá trị any được gán, gọi hoặc trả về.
Những lỗi hay gặp
- Sinh kiểu từ một bản ghi duy nhất. Một bản ghi không thể cho thấy trường tùy chọn hay trường null có giá trị. Hãy dán nhiều bản ghi, tốt nhất là các trường hợp biên: danh sách rỗng, đơn bị hủy, khách hàng không có địa chỉ.
- Coi kiểu được sinh là hợp đồng. Nếu API công bố tài liệu OpenAPI hoặc JSON Schema, hãy sinh từ đó; một mẫu chỉ là phương án dự phòng.
- Dùng `{}` cho đối tượng rỗng. Trong TypeScript
{}nghĩa là "mọi giá trị khác null". Công cụ viếtRecord<string, unknown>thay vào đó. - Đặt tên kiểu trùng kiểu có sẵn. Khóa
datesẽ cho rainterface Date, gộp vớiDatecủa JavaScript trong toàn chương trình. Công cụ đổi thànhDateType. - Mặc định là camelCase. Nhiều API gửi
snake_case. Đổi tên thuộc tính trong kiểu mà không chuyển đổi dữ liệu sẽ cho bạn kiểu của những khóa không hề tồn tại.
Thử ngay
Dán một phản hồi từ API của bạn vào công cụ JSON sang TypeScript, chuyển qua lại giữa TypeScript, Zod và Go để xem mỗi ngôn ngữ ghi lại cùng các sự thật thế nào, và nếu bộ phân tích báo lỗi, hãy kiểm tra JSON trong công cụ định dạng JSON trước.