JSON.parse trả về any, nên TypeScript chấp nhận mọi kiểu bạn gán cho kết quả. Nhờ vậy, khai báo kiểu cho JSON sau khi parse chỉ mất một dòng, nhưng cũng có nghĩa kiểu đó là lời hứa của bạn, không phải thứ trình biên dịch kiểm tra:
Đối tượng thứ hai có age là chuỗi "41". TypeScript vẫn coi nó là number, vì any gán được cho mọi kiểu, và chương trình in ra 411. Với JSON do chính code của bạn vừa tạo ra, chú thích kiểu như vậy là đủ. Với dữ liệu từ request, từ tệp hay từ local storage, hãy kiểm tra nó.
Parse vào unknown
Khai báo kết quả là unknown buộc trình biên dịch đòi một bước kiểm tra trước khi bạn dùng bất kỳ thuộc tính nào:
Lỗi in ra là index.ts(4,13): error TS18046: 'data' is of type 'unknown'. Mỗi bước kiểm tra bạn viết sau đó lại thu hẹp data thêm một chút.
Kiểm tra bằng type guard
Type guard là một hàm trả về value is User. Khi hàm trả về true, TypeScript coi giá trị là User từ đó trở đi, và các bước kiểm tra bên trong hàm là kiểm tra thật lúc chạy:
Bản thân JSON.parse ném SyntaxError khi văn bản sai định dạng, nên code thực tế còn bọc nó trong try/catch. Với dữ liệu lớn hoặc lồng nhiều tầng, type guard viết tay sẽ rất dài; các thư viện schema như Zod hay Valibot cho phép bạn khai báo cấu trúc một lần rồi suy ra cả hàm kiểm tra lẫn kiểu TypeScript từ đó.
Chuyển JSON sang interface TypeScript
Chuyển một mẫu JSON thành kiểu là việc làm theo quy tắc. Với response này:
{
"id": 42,
"title": "Learn TypeScript",
"done": false,
"owner": { "id": 7, "name": "Ada" },
"tags": ["study", "ts"],
"dueDate": "2024-03-15T10:30:00Z",
"notes": null
}
Ánh xạ mỗi giá trị sang kiểu của nó, cho mỗi đối tượng lồng nhau một interface riêng, và đánh dấu những gì có thể thay đổi:
Một mẫu duy nhất không cho bạn biết trường nào là tùy chọn hay có thể null. Hãy xem vài response khác, hoặc tài liệu của API, trước khi quyết định dùng ? và | null.
Ngày tháng và hàm reviver
JSON không có kiểu ngày tháng, nên ngày tháng đến dưới dạng chuỗi. Tham số thứ hai của JSON.parse, gọi là reviver, được gọi cho từng khóa và có thể dựng lại chúng:
Tham số value của reviver có kiểu any, kết quả cũng vậy, nên kiểu Order vẫn chỉ được tin chứ không được kiểm tra. JSON.stringify(order, null, 2) thụt lề đầu ra hai dấu cách và chuyển Date trở lại chuỗi ISO.
JSON.stringify và những gì bị mất
JSON.stringify được khai báo trả về string. Các giá trị nó chuyển đổi không phải lúc nào cũng quay về như cũ, và kiểu không cảnh báo bạn điều đó:
| Giá trị | Sau JSON.stringify |
|---|---|
Date | Chuỗi ISO (qua phương thức toJSON của nó) |
Map, Set | {} (chuyển trước bằng [...set] hoặc Object.fromEntries(map)) |
undefined, hàm, symbol trong đối tượng | khóa bị bỏ qua |
undefined, hàm, symbol trong mảng | null |
undefined, một hàm hoặc một symbol đứng riêng | undefined, không phải chuỗi |
NaN, Infinity | null |
bigint | ném TypeError |
Các quy tắc lúc chạy giống hệt JavaScript thuần, được trình bày trong JSON trong JavaScript.
Một kiểu cho mọi giá trị JSON
Khi code xử lý JSON bất kỳ, một kiểu đệ quy mô tả chính xác những gì JSON có thể chứa và từ chối những giá trị nó không chứa được:
Nếu bỏ chú thích, dòng cuối là lỗi biên dịch vì một đối tượng Date không phải là JsonValue.
Import tệp .json
Tệp .json có thể được import như một module, và TypeScript suy ra kiểu của nó từ nội dung:
{ "name": "app", "port": 8080, "tags": ["a"] }
// CommonJS output, or a bundler
import config from "./config.json";
const port: number = config.port; // typed from the file: number
// An ES module under module: nodenext
import settings from "./config.json" with { type: "json" };
Trong TypeScript 7, cách này chạy mà không cần thiết lập thêm khi module là nodenext, node20, commonjs, esnext hoặc preserve. Với node16 và node18, nó báo lỗi TS2732, Cannot find module './config.json'. Consider using '--resolveJsonModule' to import module with '.json' extension., cho đến khi bạn thêm "resolveJsonModule": true; đặt giá trị này thành false sẽ tắt việc import JSON ở mọi nơi. Trong một ES module dùng nodenext hoặc node20, câu lệnh import cần thuộc tính with { type: "json" } (thiếu nó sẽ gặp lỗi TS1543) và chỉ cho phép default import (lỗi TS1544 với import { port }). tsc sao chép tệp .json được import vào outDir, cạnh JavaScript đã biên dịch.
Câu hỏi thường gặp
JSON.parse trả về kiểu gì trong TypeScript?
any. Trình biên dịch không thể biết một chuỗi chứa gì, nên const user: User = JSON.parse(text) vẫn biên dịch được dù văn bản chứa gì đi nữa. Khi dữ liệu đến từ bên ngoài chương trình, hãy gán kết quả cho unknown rồi kiểm tra nó.
Làm sao để chuyển JSON thành interface TypeScript?
Lấy một mẫu dữ liệu tiêu biểu và viết một thuộc tính cho mỗi khóa: chuỗi, số và boolean tương ứng với string, number và boolean, đối tượng lồng nhau thành một interface riêng, mảng các đối tượng thành Item[], còn khóa thỉnh thoảng bị thiếu thì thêm ?. Các công cụ sinh code như quicktype làm việc này tự động, nhưng hãy đối chiếu kết quả đoán của chúng với nhiều hơn một mẫu.
Làm sao để import tệp JSON trong TypeScript?
import config from "./config.json"; chạy được trong TypeScript 7 khi module là nodenext, node20, commonjs, esnext hoặc preserve, và kết quả có kiểu suy ra từ nội dung tệp. Với node16 hoặc node18, bạn cần đặt thêm "resolveJsonModule": true. Trong một ES module dùng nodenext hoặc node20, hãy thêm thuộc tính mà Node yêu cầu: import config from "./config.json" with { type: "json" };.
JSON.stringify có luôn trả về chuỗi không?
Kiểu của nó ghi là string, nhưng JSON.stringify(undefined) và JSON.stringify(() => 1) trả về undefined lúc chạy. Các giá trị bên trong đối tượng cũng bị chuyển đổi: Date thành chuỗi ISO, còn Map và Set thành {}.