
停止手写TypeScript接口:使用JSON生成器代替
📷 Roman Synkevych / Pexels停止手写TypeScript接口:使用JSON生成器代替
从JSON有效载荷手动编写TypeScript接口既繁琐又容易出错。以下是如何自动化它以及需要注意的事项。
如果你曾经花二十分钟从刚从API收到的JSON有效载荷手写TypeScript接口,你已经知道这是那些看起来重要但几乎完全是机械性的任务之一。你没有在思考,只是在复制字段名并推断类型。这是机器应该做的工作。
然而,许多开发者仍然手动做这件事。本指南介绍为什么停止这样做很值得,JSON to TypeScript生成器实际上生成什么,以及——更重要的是——它不能做什么,这样你就知道该在哪里接手。
TypeScript接口为什么真的很重要
在了解工具之前,值得明确从类型化接口中得到的东西,不仅仅是"捕获错误"。
IDE自动补全是被低估的一个。 当你的API响应被类型化时,你的编辑器确切知道哪些字段存在以及嵌套对象的形状。你不再需要猜测是user.profilePicture还是user.profile_picture还是user.avatar。
重构变得更安全。 如果后端团队将字段从userId重命名为user_id,代码库中所有引用旧名称的地方在编译时就会中断——而不是在用户遇到错误后才发现。
不会过时的文档。 一个命名良好的接口通常比书面文档更有用。
手动过程与使用生成器
对于中等复杂度的API响应,手写类型是这样的:
{
"user": {
"id": 1042,
"email": "alice@example.com",
"name": "Alice Nguyen",
"roles": ["admin", "editor"],
"profile": {
"bio": "Frontend developer based in Berlin.",
"avatarUrl": "https://cdn.example.com/avatars/1042.png",
"joinedAt": "2023-06-15T08:30:00Z"
},
"settings": {
"theme": "dark",
"notifications": {
"email": true,
"push": false
}
}
},
"meta": {
"requestId": "abc-123",
"timestamp": 1712486400
}
}
将同样的JSON粘贴到JSON to TypeScript工具中,不到一秒就得到这个:
export interface Root {
user: User;
meta: Meta;
}
export interface User {
id: number;
email: string;
name: string;
roles: string[];
profile: Profile;
settings: Settings;
}
export interface Profile {
bio: string;
avatarUrl: string;
joinedAt: string;
}
export interface Settings {
theme: string;
notifications: Notifications;
}
export interface Notifications {
email: boolean;
push: boolean;
}
export interface Meta {
requestId: string;
timestamp: number;
}
你所需要做的就是将Root重命名为像UserResponse这样有意义的东西。
真实世界的使用案例
API响应类型化
这是主要使用案例。你在与第三方API集成——支付处理器、CRM、天气服务——需要返回内容的类型。
配置文件
如果你的应用在运行时读取JSON配置文件,你需要一个匹配的TypeScript接口:
// config.interface.ts(从config.json生成)
export interface AppConfig {
database: Database;
redis: Redis;
featureFlags: FeatureFlags;
}
export interface Database {
host: string;
port: number;
name: string;
ssl: boolean;
}
测试数据模拟
编写测试时,你经常需要类型化的模拟对象。从真实数据生成接口,然后根据它创建你的类型化模拟。TypeScript会告诉你模拟是否缺少必需字段。
处理棘手情况
Null值
JSON允许将null作为值,生成器会将这些字段类型化为null。但实际上,null字段通常意味着字段是可选的:
interface User {
middleName: string | null;
}
日期
JSON没有原生的Date类型。"2023-06-15T08:30:00Z"就JSON而言是一个string,生成器将其类型化为string。一些团队创建类型别名:
type ISODateString = string;
可辨识联合
有时API根据type或status字段返回不同的形状:
type ApiResult = SuccessResult | ErrorResult;
interface SuccessResult {
type: "success";
data: OrderData;
}
interface ErrorResult {
type: "error";
code: string;
message: string;
}
生成类型的最佳实践
为根接口命名有意义的名称
生成器会将其命名为Root或通用名称。提交前始终重命名。UserProfileResponse、CheckoutSessionPayload、ProductListItem——使类型可查找且自解释的名称。
将类型保存在专用文件中
src/
types/
api/
user.types.ts
order.types.ts
config.types.ts
使用Zod进行运行时验证
TypeScript类型在运行时被擦除——它们不保护你免受API实际返回意外数据的影响。如果需要运行时保证,考虑Zod:
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
email: z.string().email(),
name: z.string(),
});
type User = z.infer<typeof UserSchema>;
生成器无法为你做的事情
它不知道你的业务逻辑。 类型化为number的字段在实际中可能只会是正整数。
它从快照工作。 生成的类型反映一个数据样本。真实的API会演进。
它无法区分必需和可选。 样本中的每个字段都被视为必需的。
它将所有字符串视为字符串。 只包含"pending"、"active"或"cancelled"的status字段将被类型化为string,而不是更精确的联合类型。
这些不是避免使用工具的理由——它们是将输出视为草稿而非成品的理由。生成器处理80%的繁琐工作,你处理需要理解数据实际语义的20%。
综合运用
大多数项目有意义的工作流:
- 从API、配置或其他来源获取真实(或代表性)的JSON样本
- 粘贴到JSON to TypeScript生成器
- 将
Root重命名为有意义的名称 - 将输出复制到适当的
*.types.ts文件 - 检查每个字段:用
?标记可选字段,在需要的地方添加| null,将适当的字面量字符串转换为联合类型 - 如果源JSON被压缩难以阅读,先使用JSON格式化工具,然后生成类型
- 如果同时维护YAML配置,JSON to YAML转换器可以帮助保持同步
目标是拥有准确、命名和组织好的TypeScript类型,而不在纯粹的机械转录上花时间。生成器做机械部分,你带来数据实际含义的背景知识。
手写的类型并不本质上更好——只是更慢且更容易出现拼写错误。使用工具,审查输出,把时间花在真正需要人类参与的部分。