Menu

TypeScript JSON:JSON.parse 类型、JSON 转接口与导入

JSON.parse 返回 any,所以你给结果标注什么类型,TypeScript 就相信什么类型。本文讲解如何为解析后的 JSON 标注类型、用类型守卫校验它、把 JSON 示例转换成接口、导入 .json 文件,以及 JSON.stringify 会如何处理 Date、Set 和 undefined。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

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 之后
DateISO 字符串(通过它的 toJSON 方法)
Map、Set{}(先用 [...set] 或 Object.fromEntries(map) 转换)
对象中的 undefined、函数、symbol这个键被省略
数组中的 undefined、函数、symbolnull
单独的 undefined、函数或 symbolundefined,而不是字符串
NaN、Infinitynull
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 变成 {}。

Coddy 编程语言插图

和 Coddy 一起学编程

开始