JSON.parse 返回 any,所以无论你把结果赋给什么类型,TypeScript 都会接受。这让为解析后的 JSON 标注类型只需要一行代码,但也意味着这个类型是你做出的承诺,而不是编译器检查过的东西:
第二个对象中的 age 是字符串 "41"。TypeScript 仍然认为它是 number,因为 any 可以赋值给任何类型,于是程序打印出 411。如果 JSON 是你自己的代码刚刚生成的,这样标注没有问题。如果数据来自请求、文件或本地存储,就要校验它。
解析为 unknown
把结果的类型标注为 unknown,编译器就会坚持要求你在使用任何属性之前先做检查:
错误是 index.ts(4,13): error TS18046: 'data' is of type 'unknown'.。之后你写下的每一项检查都会把 data 再收窄一点。
用类型守卫校验
类型守卫是返回 value is User 的函数。当它返回 true 时,TypeScript 从此把这个值当作 User,而函数内部的检查都是真正的运行时检查:
格式错误的文本会让 JSON.parse 本身抛出 SyntaxError,所以实际代码还要用 try/catch 包住它。对于大型或嵌套的数据,手写的类型守卫会变得很长;Zod 或 Valibot 这类 schema 库让你只声明一次形状,就能同时得到校验器和 TypeScript 类型。
JSON 转 TypeScript 接口
把 JSON 示例转换成类型是一件机械的工作。假设有这样一个响应:
{
"id": 42,
"title": "Learn TypeScript",
"done": false,
"owner": { "id": 7, "name": "Ada" },
"tags": ["study", "ts"],
"dueDate": "2024-03-15T10:30:00Z",
"notes": null
}
把每个值对应到它的类型,给嵌套对象写单独的接口,并标出可能变化的部分:
仅凭一个示例无法判断哪些字段是可选的或可能为 null。在确定 ? 和 | null 之前,多看几个响应,或者查阅 API 的文档。
日期与 reviver
JSON 没有日期类型,所以日期以字符串的形式到达。JSON.parse 的第二个参数 reviver 会对每个键调用一次,可以用它把日期重建出来:
reviver 的 value 参数是 any,结果也是 any,所以 Order 类型仍然是被信任的,而不是被检查的。JSON.stringify(order, null, 2) 用两个空格缩进输出,并把 Date 转回 ISO 字符串。
JSON.stringify 会丢失什么
JSON.stringify 的返回类型是 string。它转换的值并不总能原样还原,而类型不会提醒你:
| 值 | 经过 JSON.stringify 之后 |
|---|---|
Date | ISO 字符串(通过它的 toJSON 方法) |
Map、Set | {}(先用 [...set] 或 Object.fromEntries(map) 转换) |
对象中的 undefined、函数、symbol | 这个键被省略 |
数组中的 undefined、函数、symbol | null |
单独的 undefined、函数或 symbol | undefined,而不是字符串 |
NaN、Infinity | null |
bigint | 抛出 TypeError |
运行时的规则和普通 JavaScript 相同,详见 JavaScript 中的 JSON。
表示任意 JSON 值的类型
当代码要处理任意 JSON 时,可以用一个递归类型准确描述 JSON 能容纳的内容,并拒绝它不能容纳的值:
如果没有那条注释,最后一行就是编译错误,因为 Date 对象不是 JsonValue。
导入 .json 文件
.json 文件可以像模块一样导入,TypeScript 会根据文件内容推断它的类型:
{ "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" };
在 TypeScript 7 中,当 module 设为 nodenext、node20、commonjs、esnext 或 preserve 时,这不需要额外设置。在 node16 和 node18 下,它会报错 TS2732:Cannot find module './config.json'. Consider using '--resolveJsonModule' to import module with '.json' extension.,直到你加上 "resolveJsonModule": true;把它设为 false 则会在所有情况下关闭 JSON 导入。在 nodenext 或 node20 下的 ES 模块中,导入需要 with { type: "json" } 属性(缺少时报错 TS1543),并且只允许默认导入(import { port } 会报错 TS1544)。tsc 会把导入的 .json 文件复制到 outDir,放在编译出的 JavaScript 旁边。
常见问题
在 TypeScript 中 JSON.parse 返回什么类型?
any。编译器无法知道一个字符串里装的是什么,所以无论文本内容是什么,const user: User = JSON.parse(text) 都能编译通过。当数据来自程序外部时,把结果赋给 unknown 并加以校验。
如何把 JSON 转换成 TypeScript 接口?
取一个有代表性的示例,每个键写一个属性:字符串、数字和布尔值分别对应 string、number 和 boolean,嵌套对象写成单独的接口,对象数组写成 Item[],有时会缺失的键加上 ?。quicktype 这类代码生成工具可以自动完成,但要用多个示例检验它的推测。
如何在 TypeScript 中导入 JSON 文件?
在 TypeScript 7 中,当 module 设为 nodenext、node20、commonjs、esnext 或 preserve 时,import config from "./config.json"; 可以直接使用,结果的类型根据文件内容推断。使用 node16 或 node18 时,还要设置 "resolveJsonModule": true。在 nodenext 或 node20 下的 ES 模块中,要加上 Node 要求的属性:import config from "./config.json" with { type: "json" };。
JSON.stringify 总是返回字符串吗?
它的类型写的是 string,但运行时 JSON.stringify(undefined) 和 JSON.stringify(() => 1) 会返回 undefined。对象内部的值也会被转换:Date 变成 ISO 字符串,Map 和 Set 变成 {}。