JSON 转 TypeScript 从 API 响应自动生成接口
为每个API响应手动编写TypeScript接口既繁琐又容易出错。学习如何从真实的JSON数据自动生成准确的接口,然后使用Zod添加运行时验证——因为类型在运行时会消失,而`any`并不是解决方案。
您刚刚从第三方API获取了数据。响应是一个密集的JSON块——嵌套的对象、数组、可空字段——现在您必须弄清楚如何在TypeScript中为其定义类型。因此,您打开一个新文件,开始敲代码 interface User { ... },20分钟后,您得到了一个可能与实际数据匹配的结果。可能。
有一种更好的方法。能够将JSON直接转换为TypeScript接口的工具,将这个耗时20分钟的步骤缩短到几秒钟。本文将介绍这些生成类型的外观,如何处理棘手的情况(空值、联合类型、深层嵌套),以及为什么您应该将生成的类型与Zod模式结合使用,以在运行时捕获形状不匹配的问题——而不仅仅是编译时。
TypeScript接口在API响应中的重要性
TypeScript的核心优势在于在代码运行之前发现错误。如果没有经过类型化的API响应,您将处于盲区:访问可能不存在的属性,将可选值当作必需值处理,或在下游静默地将字符串转换为意外类型。 "null" 转换为某种意想不到的类型。
考虑以下常见场景:
const user = await fetchUser(id);
console.log(user.address.city); // TypeError at runtime if address is null
如果您正确地为响应定义了类型——使用 address: Address | null ——TypeScript会立即标记该访问操作。编译器是您的第一道防线,但前提是您为其提供了可用的类型信息。
为每个API手动编写接口既繁琐又容易出错。您可能误解了模式,遗漏了一个可选字段,或复制了过时的版本。直接从真实JSON数据生成接口消除了这种人为错误。
JSON到TypeScript转换生成的内容
考虑一个简单的API响应:
{
"id": 42,
"username": "jsmith",
"email": "j@example.com",
"createdAt": "2024-01-15T10:30:00Z",
"role": "admin",
"profile": {
"bio": "Developer",
"avatar": null
}
}
将其粘贴到 JSON 转 TypeScript 接口生成器 中,您会得到:
interface Profile {
bio: string;
avatar: null;
}
interface RootObject {
id: number;
username: string;
email: string;
createdAt: string;
role: string;
profile: Profile;
}
几点需要注意:
- 嵌套对象会变成独立的接口 —
Profile会自动提取而不是内联。 - 日期被类型化为
string——JSON中没有日期类型,因此ISO字符串仍保持为字符串。您需要自行解析它们。 avatar: null被类型化为字面量null——这虽然准确但不完整。更多详情见下文。
处理棘手的情况
可空字段
当您的样本JSON中某个字段为 null 时,生成器将其类型化为 null。但在实际使用中,该字段可能根据数据在真实值和null之间切换。您需要手动调整这些类型:
// Generated
avatar: null;
// What you actually want
avatar: string | null;
对于在样本中存在但可能在某些响应中缺失的可选字段,也应添加 ? 到任何可能在某些响应中缺失的属性上。
数组
对象数组的处理是干净的。给定:
{
"posts": [
{ "id": 1, "title": "Hello", "published": true },
{ "id": 2, "title": "World", "published": false }
]
}
生成器产生:
interface Post {
id: number;
title: string;
published: boolean;
}
interface RootObject {
posts: Post[];
}
联合类型
如果您的JSON样本显示某个字段在不同记录中持有不同类型的值——例如,一个 value 可以是数字或字符串——您应该将其表示为联合类型。单个样本无法被生成类型捕获这种情况,因此值得检查API文档:
value: string | number;
深层嵌套对象
深层嵌套是手动类型化真正崩溃的地方——而生成器在此处大显身手。一个具有三层或四层嵌套的响应会被分解为一个清晰的命名接口层级,每个接口负责其自身的形状。
编译时类型与运行时现实之间的差距
这里往往是TypeScript新手容易忽略的一点: 类型在运行时消失。 TypeScript编译为JavaScript,而JavaScript没有接口的概念。如果API返回的形状与您声明的类型不匹配,TypeScript将无法察觉——也不会告诉您。
这使得在API响应上进行类型转换的常见模式变得真正危险:
const data = await response.json() as User; // No validation, just trust
该转换告诉TypeScript“相信我,这是一个 User”——但TypeScript没有任何方式来验证这一点。如果API改变了其形状或返回了一个错误对象,您的代码将在运行时以编译器从未警告过的方式崩溃。
解决方案是运行时验证。
Zod用于运行时验证
Zod 是一个以TypeScript为基础的模式验证库。您只需定义一次模式,使用它来解析传入的数据,即可获得一个完全类型化的值——或者在形状不匹配时获得详细的错误信息。无需类型转换,无需猜测。
使用之前相同的JSON样本,Zod生成如下内容: JSON 到 Zod Schema 生成器 请注意最后一行:
import { z } from "zod";
const ProfileSchema = z.object({
bio: z.string(),
avatar: z.null(),
});
const RootObjectSchema = z.object({
id: z.number(),
username: z.string(),
email: z.string(),
createdAt: z.string(),
role: z.string(),
profile: ProfileSchema,
});
type RootObject = z.infer<typeof RootObjectSchema>;
直接从模式推导出TypeScript类型。您从单一来源获得了编译时类型安全和运行时验证。 z.infer 在获取数据的边界处使用它看起来如下:
调整生成的模式以处理生成器无法从单个样本推断出的可空情况:
const rawData = await response.json();
const user = RootObjectSchema.parse(rawData); // throws if shape is wrong
// Or use safeParse to avoid throwing:
const result = RootObjectSchema.safeParse(rawData);
if (!result.success) {
console.error(result.error.issues);
} else {
console.log(result.data.username); // fully typed
}
接口与类型:您应该使用哪一个?
avatar: z.string().nullable(), // was z.null()
bio: z.string().optional(), // if the field might be absent
别名可以表示TypeScript中的对象形状,对于大多数API响应类型化来说,它们是可互换的。实际差异如下:
两者相同 interface 且 type 接口可以被扩展和合并
- ——如果您希望在多个文件中扩展基础类型,这非常有用。声明合并允许您向其他地方定义的接口添加字段。 类型别名更灵活
- ——它们可以表示联合类型、交集类型、元组类型和映射类型,而接口无法做到这一点。 错误信息通常更清晰
- ——TypeScript在错误输出中展开类型别名,这可能会使深层嵌套的错误更难阅读。 对于API响应形状,两者都可以使用。如果预计要扩展类型,请选择
;如果需要联合或交集语义,请选择 interface 。代码库内部的一致性比选择哪一个更重要。 type 实际工作流程
以下是一个将耗时从下午缩短到几分钟的工作流程:
获取一个真实的响应。
- 使用浏览器的开发者工具网络标签页、Postman或curl捕获一个真实的API响应。样本越完整,生成的类型就越准确。 生成TypeScript接口。
- 将JSON粘贴到 中。将输出复制到您的项目中。 JSON 转 TypeScript 接口生成器生成Zod模式。
- 将相同的JSON粘贴到 中。也将其复制到您的项目中。 JSON 到 Zod Schema 生成器检查可空和可选字段。
- 检查生成输出中是否包含被类型化为字面量 或可能缺失的字段。将这些字段更新为
null验证在获取数据的边界处。string | null,.nullable(), 或者.optional()根据需要。 - 将任何 转换替换为
as YourType。现在类型是被保证匹配的,而不是被假设的。YourSchema.parse()或safeParse()这就是完整的流程——从原始JSON到编译时安全和运行时保证,仅需几分钟。
这就是完整的循环——从原始JSON到编译时的安全性和运行时保证,仅需几分钟。
