← 全部文章

如何从 JSON 生成 TypeScript 接口

学习如何把 JSON 转成 TypeScript 接口 —— 手写、在线工具或代码生成。涵盖嵌套对象、可选字段、数组、可空类型,以及如何让类型与 API 保持同步。

每个 TypeScript 开发者都遇到过这种情况:你正在对接一个新 API,手上有一份示例响应,在写任何业务逻辑之前都需要先定义好类型化的接口。手动编写这些接口又乏味、又重复、又容易出错 —— 尤其是响应嵌套很深或者字段数量很多的时候。本文涵盖把 JSON 转换为 TypeScript 接口的所有实用方法:输出应该是什么样子、如何自动生成,以及如何处理棘手的情况。

为什么 JSON 数据需要 TypeScript 接口

在 TypeScript 中,不加类型注解就调用 API,解析得到的响应类型是 any。这意味着没有自动补全、没有类型检查、也没有编译时的安全性。属性名拼写错误会变成运行时 bug,而不是编译错误。

// 没有类型 —— 一切都是 any
const response = await fetch('/api/users/42');
const user = await response.json(); // user: any
console.log(user.naem); // 拼写错误 —— 编译时无报错

// 用类型化的接口
interface User {
  id: number;
  name: string;
  email: string;
}

const response = await fetch('/api/users/42');
const user: User = await response.json();
console.log(user.naem); // 错误: Property 'naem' does not exist on type 'User'

接口在运行时没有任何开销 —— 它只存在于编译期。但它完全改变了开发体验。

JSON 转 TypeScript 后会生成什么

每个 JSON 对象都会变成一个 TypeScript interface,每种 JSON 基本类型映射到对应的 TypeScript 类型,数组则推断其元素类型。

// 输入的 JSON
{
  "id": 42,
  "name": "Alice Johnson",
  "email": "alice@example.com",
  "active": true,
  "score": 98.5,
  "address": {
    "street": "123 Main St",
    "city": "New York",
    "zip": "10001"
  },
  "tags": ["admin", "editor"],
  "orders": [
    { "id": 101, "total": 49.99, "status": "shipped" },
    { "id": 102, "total": 99.00, "status": "pending" }
  ]
}
// 输出的 TypeScript
interface Root {
  id: number;
  name: string;
  email: string;
  active: boolean;
  score: number;
  address: Address;
  tags: string[];
  orders: Order[];
}

interface Address {
  street: string;
  city: string;
  zip: string;
}

interface Order {
  id: number;
  total: number;
  status: string;
}

每个嵌套对象都会成为一个命名接口。名字来自父级键 —— address 变成 Address,orders 中的元素变成 Order。这让类型保持可读、可组合。

类型映射规则

理解 JSON 类型如何映射到 TypeScript 类型,可以让输出可预测,也方便你放心地调整它。

JSON 值TypeScript 类型
"hello"string
42, 3.14number
true, falseboolean
nullnull
{}命名 interface
['a', 'b']string[]
[1, 'two'](number | string)[]
[](空)unknown[]

一个细节:TypeScript 没有整数类型。423.14 都映射成 number。如果这个区分对你的场景重要,可以使用品牌类型或加注释。

方法一:使用在线 JSON 转 TypeScript 工具

对于一次性任务 —— 新的 API 端点、快速原型 —— 在线工具是最快的途径。fixjson 的 JSON 转 TypeScript 工具 完全在浏览器里完成转换:

  1. 粘贴你的 JSON 到输入面板。工具接受合法 JSON,也接受含有常见问题(如尾随逗号、单引号)的损坏 JSON —— 转换前会先修复。
  2. TypeScript 接口立即出现 在右侧 —— 无需点击按钮。
  3. 复制输出 并直接粘贴到你的 TypeScript 文件中。

所有操作都在本地运行。任何数据都不会发送到服务器 —— 这点在你处理的 JSON 含有 API 密钥、用户数据或内部配置时尤为重要。

方法二:用 TypeScript 自己写转换器

当你需要以编程方式生成类型 —— 作为构建脚本或代码生成器的一部分 —— 可以自己实现这个逻辑。下面是一个最小但完整的实现:

function jsonToTypeScript(value: unknown, rootName = 'Root'): string {
  const interfaces: string[] = [];
  const seen = new Set<string>();

  function cap(s: string) {
    return s ? s.charAt(0).toUpperCase() + s.slice(1) : 'Unknown';
  }

  function singular(name: string) {
    if (/ies$/i.test(name)) return name.slice(0, -3) + 'y';
    if (/[^s]s$/i.test(name)) return name.slice(0, -1);
    return name + 'Item';
  }

  function getType(val: unknown, name: string): string {
    if (val === null) return 'null';
    if (typeof val === 'boolean') return 'boolean';
    if (typeof val === 'number') return 'number';
    if (typeof val === 'string') return 'string';
    if (Array.isArray(val)) {
      if (val.length === 0) return 'unknown[]';
      const objs = val.filter(
        (v): v is Record<string, unknown> =>
          v !== null && typeof v === 'object' && !Array.isArray(v),
      );
      if (objs.length > 0) {
        const itemName = cap(singular(name));
        buildInterface(itemName, objs);
        return `${itemName}[]`;
      }
      const types = [...new Set(val.map((v) => getType(v, name)))];
      return types.length === 1 ? `${types[0]}[]` : `(${types.join(' | ')})[]`;
    }
    if (typeof val === 'object') {
      const iName = cap(name);
      buildInterface(iName, [val as Record<string, unknown>]);
      return iName;
    }
    return 'unknown';
  }

  function buildInterface(name: string, objs: Record<string, unknown>[]): void {
    if (seen.has(name)) return;
    seen.add(name);
    const allKeys = [...new Set(objs.flatMap((o) => Object.keys(o)))];
    const props: string[] = [];
    for (const key of allKeys) {
      const present = objs.filter((o) => key in o);
      const optional = present.length < objs.length ? '?' : '';
      const propType = getType(present[0][key], cap(key));
      const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : `"${key}"`;
      props.push(`  ${safeKey}${optional}: ${propType};`);
    }
    interfaces.push(`interface ${name} {\n${props.join('\n')}\n}`);
  }

  const rootType = getType(value, rootName);
  const defs = interfaces.reverse().join('\n\n');
  if (rootType !== cap(rootName)) {
    return `type ${cap(rootName)} = ${rootType};\n\n${defs}`.trim();
  }
  return defs;
}

在 Node.js 构建脚本中使用:

import * as fs from 'fs';

const json = JSON.parse(fs.readFileSync('api-response.json', 'utf8'));
const typescript = jsonToTypeScript(json, 'UserResponse');
fs.writeFileSync('types/user.ts', typescript);

方法三:quicktype(CLI 和库)

quicktype 是功能最完善的开源工具,可从 JSON 生成 TypeScript 类型。它能处理简单生成器遗漏的边角情况,并支持多种输出格式。

npm install -g quicktype

# 从本地文件
quicktype api-response.json -o types/user.ts --lang typescript

# 从 URL
quicktype https://api.example.com/users/42 -o types/user.ts --lang typescript

# 从 stdin
cat api-response.json | quicktype --lang typescript

quicktype 还可以使用 io-ts 或 zod 等库生成运行时校验函数 —— 当你需要校验传入数据而不仅仅是静态类型化时非常有用。

处理棘手的情况

可空字段

JSON API 经常用 null 表示缺失的值。生成器看到 null 时只会生成 null 作为类型 —— 但你通常想要的是 string | null:

// 自动生成(初版 —— 太窄)
interface Root {
  name: string;
  middleName: null;
}

// 你应该改写为
interface Root {
  name: string;
  middleName: string | null;
}

生成后务必人工检查可空字段。如果你有多份示例响应,字段有时是字符串、有时是 null,好的生成器会自动推断出 string | null

可选 vs 可空

TypeScript 区分 field?: string(键可能缺失)和 field: string | null(键始终存在,值可能为 null):

interface Order {
  id: number;
  total: number;
  discount?: number;   // 部分响应中没有这个键
  note: string | null; // 键总是存在,值可能为 null
}

能分析多份 JSON 样本的生成器可以准确推断出这种区别。单样本生成器会把缺失的键标记为可选,但无法区分 “键缺失” 和 “值为 null”。

不是合法标识符的键

有些 JSON 键包含 TypeScript 属性名中不合法的字符:

// JSON
{ "content-type": "application/json", "x-request-id": "abc123" }

// 生成的 TypeScript —— 合法,但需要使用方括号访问
interface Root {
  "content-type": string;
  "x-request-id": string;
}

// 通过方括号访问
obj["content-type"]

如果你能控制 API,优先使用驼峰命名以彻底避免这个问题。如果不能,带引号的属性写法在 TypeScript 中完全合法且可用。

混合类型的数组

// JSON
{ "values": [1, "two", true, null] }

// 生成结果
interface Root {
  values: (number | string | boolean | null)[];
}

这样的联合类型在技术上正确,但通常意味着 API 设计有问题。看到这种情况时,先确认字段是真的混合类型,还是样本数据具有误导性。

安全地使用生成的类型

生成接口只是第一步。接口是编译期契约 —— 它在运行时不会校验任何东西。response.json() 返回 any,TypeScript 会允许你把它断言为你的接口而不进行实际检查。

对于双方都受你控制的内部 API,这通常没问题。对于外部 API 或用户提供的数据,要在运行时进行校验。推荐使用 Zod:

import { z } from 'zod';

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  active: z.boolean(),
  address: z.object({
    street: z.string(),
    city: z.string(),
    zip: z.string(),
  }),
  tags: z.array(z.string()),
  orders: z.array(z.object({
    id: z.number(),
    total: z.number(),
    status: z.string(),
  })),
});

type User = z.infer<typeof UserSchema>; // 从 schema 推导的 TypeScript 类型

const raw = await response.json();
const user = UserSchema.parse(raw); // 形状不匹配就抛错

Zod 的做法是两全其美:schema 是 TypeScript 类型和运行时校验的单一真相来源。如果 API 发生破坏性变更,你能在数据进入系统的那一刻就知道。

超越接口:校验器和类型化客户端

生成的接口是编译期契约。更进一步的方案是用一些库,从单一来源同时获取类型和运行时检查:

  • Zod io-ts —— 定义 schema,从中推导 TypeScript 类型,并在边界处校验传入数据。如果 API 改变,你的构建(以及错误日志)会立即告诉你。
  • tRPC 类型化的 fetch 封装(例如 openapi-fetch)—— 服务端的过程或 OpenAPI 定义是真相来源,客户端自动获得匹配的类型,中间没有 JSON 到 TS 的转换步骤。
  • ts-pattern 和可辨识联合 —— 一旦数据带有标签字段(例如 { kind: "circle" | "square", ... }),穷尽式模式匹配可以在编译时捕获遗漏的分支。

让类型与 API 保持同步

生成的类型有保质期。API 会变 —— 字段被重命名、新增必填字段,或者字符串字段变成可空。你的接口会悄无声息地过时。

从 OpenAPI 规范生成,而不是从示例响应生成

如果 API 有 OpenAPI (Swagger) 定义,openapi-typescript 可以直接从规范生成类型:

npm install -g openapi-typescript
openapi-typescript https://api.example.com/openapi.json -o types/api.ts

这比从示例响应生成可靠得多,因为规范会记录每个字段,包括可选项、可空变体和枚举值。

在 CI 中重新生成类型

在 CI 流水线中加一步:从规范或已知正确的样本重新生成类型,然后运行 tsc --noEmit 检查是否破坏。如果 API 变了,构建会在破坏性变更进入生产之前失败。

常见问题

如何把 JSON 转换成 TypeScript 接口?

把 JSON 粘贴进像 fixjson 的 JSON 转 TypeScript 工具 这样的在线工具可立刻得到结果;在构建步骤中运行 quicktype;或者对长期代码用 openapi-typescript 从 OpenAPI 规范生成。

JSON 类型如何映射到 TypeScript 类型?

字符串 → string,所有数字 → number(TypeScript 没有整数类型),布尔 → boolean,对象 → 命名接口,数组推断其元素类型。请参见上面的映射表和 JSON 的六种数据类型

如何处理可空或可选字段?

键可能缺失时用 field?: T;键始终存在但值可能为 null 时用 field: T | null。能分析多个样本的生成器会推断出这一点;单样本工具则需要人工检查。

TypeScript 接口会在运行时校验 JSON 吗?

不会 —— 接口在编译时就被擦除了。对于外部数据需要运行时安全,用 Zod 校验,或用 JSON Schema 描述形状。

结语

把 JSON 转换为 TypeScript 接口是每个 TypeScript 开发者经常做的事。合适的方法取决于场景:对于快速一次性的对接,在线转换器 一分钟内就能让你开始干活。对于构建流水线,quicktype 或自定义脚本能自动化整个过程。对于长期生产代码,从 OpenAPI 规范生成类型并用 Zod 在运行时校验,能给你最强的保障。

无论用哪种方法,生成的接口都只是起点 —— 检查可空字段、为非显而易见的属性加上注释、在业务规则比 JSON 样本更严格的地方收紧约束。准确反映数据的类型让整个代码库更易维护。

如果你的 JSON 本身在尝试转换前就有语法错误,先修复它。TypeScript 生成器 —— 就像任何 JSON 解析器一样 —— 需要语法上合法的输入。