转换器根据样本值生成初稿。以下手动调整需要了解 API 契约;推断的类型不会在运行时验证数据。
让我们从基础开始。给定一个 JSON 对象,如何将它转换为 TypeScript 接口?
这是一个来自 API 的简单用户响应:
{
"id": 42,
"username": "jdoe",
"email": "[email protected]",
"isActive": true
}
对应的 TypeScript 接口:
interface User {
id: number;
username: string;
email: string;
isActive: boolean;
}
每个 JSON 值映射到一个 TypeScript 原始类型:number、string 或 boolean。对象键变成属性名。
真实的 API 很少返回扁平结构。考虑一个带有地址的用户:
{
"id": 42,
"username": "jdoe",
"address": {
"street": "123 Main St",
"city": "Springfield",
"zipCode": "62704",
"country": "US"
}
}
将嵌套对象拆分为独立的接口:
interface Address {
street: string;
city: string;
zipCode: string;
country: string;
}
interface User {
id: number;
username: string;
address: Address;
}
这使每个类型保持专注和可复用。如果另一个端点也返回 Address,你可以使用同一个接口。
当字段包含数组时,标注元素类型:
{
"id": 42,
"username": "jdoe",
"roles": ["admin", "editor"],
"posts": [
{ "id": 1, "title": "Hello World" },
{ "id": 2, "title": "TypeScript Tips" }
]
}
interface Post {
id: number;
title: string;
}
interface User {
id: number;
username: string;
roles: string[];
posts: Post[];
}
简单情况使用 Type[] 语法。对于复杂泛型,Array<Type> 可能更清晰,但一致性比你选择哪种更重要。
API 经常返回可能存在也可能不存在的字段。用 ? 标记它们:
{
"id": 42,
"username": "jdoe",
"bio": "Software developer"
}
interface User {
id: number;
username: string;
bio?: string; // 可能在响应中不存在
}
可选字段(bio?: string)意味着键可能完全不存在。这与可空字段不同,可空字段的键存在但值可能是 null。
生产 API 是混乱的。以下是如何处理常见问题。
当字段可以显式为 null 时:
{
"id": 42,
"username": "jdoe",
"avatarUrl": null,
"deletedAt": null
}
interface User {
id: number;
username: string;
avatarUrl: string | null;
deletedAt: string | null; // ISO 日期字符串或 null
}
如果字段既可以不存在又可以为 null,则将两者结合:
interface User {
id: number;
username: string;
middleName?: string | null; // 可能不存在,或存在但为 null
}
某些字段接受多种类型。搜索 API 可能返回混合结果:
{
"results": [
{ "type": "user", "id": 1, "username": "jdoe" },
{ "type": "post", "id": 2, "title": "Hello" }
]
}
用可辨识联合进行建模:
interface UserResult {
type: 'user';
id: number;
username: string;
}
interface PostResult {
type: 'post';
id: number;
title: string;
}
type SearchResult = UserResult | PostResult;
interface SearchResponse {
results: SearchResult[];
}
type 字段充当辨别器。TypeScript 在你检查它时自动缩窄类型:
function renderResult(result: SearchResult) {
if (result.type === 'user') {
console.log(result.username); // TypeScript 知道这是 UserResult
} else {
console.log(result.title); // TypeScript 知道这是 PostResult
}
}
当 API 返回固定的字符串值集合时,使用字符串字面量联合而不是 TypeScript 枚举:
{ "status": "active" }
type UserStatus = 'active' | 'inactive' | 'suspended';
interface User {
id: number;
status: UserStatus;
}
字符串字面量联合更简单、可被 tree-shaking、不像 enum 那样生成运行时 JavaScript。仅在需要反向映射(数字到名称)时才使用 enum。
interface 和 type 都可以描述对象形状。以下是何时选择哪个:
对可能扩展的对象形状使用 interface:
interface BaseEntity {
id: number;
createdAt: string;
updatedAt: string;
}
interface User extends BaseEntity {
username: string;
email: string;
}
对联合、交叉和计算类型使用 type:
type ApiResponse<T> = {
data: T;
meta: { total: number; page: number };
};
type UserOrAdmin = User | Admin;
实际上,大多数情况下两者都可以。为你的项目选择一个约定并坚持下去。许多团队对数据模型使用 interface,对其他一切使用 type。
为了最大安全性,使用 Zod 定义形状并从中推断 TypeScript 类型:
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
username: z.string(),
email: z.string().email(),
isActive: z.boolean(),
bio: z.string().nullable().optional(),
});
type User = z.infer<typeof UserSchema>;
// 现在在运行时验证
const response = await fetch('/api/users/42');
const data = await response.json();
const user = UserSchema.parse(data); // 如果形状不匹配则抛出错误
使用 Zod,你的类型和验证逻辑在同一个地方。如果 API 改变了形状,parse() 会立即抛出错误,而不是让坏数据传播到你的应用中。
类型只有在你将它们应用于数据进入应用的边界时才有用。
创建一个强制执行类型的通用 fetch 函数:
async function fetchJson<T>(url: string, schema?: z.ZodType<T>): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (schema) {
return schema.parse(data);
}
return data as T;
}
// 用法
const user = await fetchJson<User>('/api/users/42');
const validatedUser = await fetchJson('/api/users/42', UserSchema);
如果使用 Axios,将类型应用于响应泛型:
import axios from 'axios';
interface PaginatedResponse<T> {
data: T[];
meta: {
total: number;
page: number;
perPage: number;
lastPage: number;
};
}
async function getUsers(page: number = 1) {
const response = await axios.get<PaginatedResponse<User>>('/api/users', {
params: { page },
});
return response.data; // 完全类型化
}
在 React 应用中,将类型与数据获取库配对:
import { useQuery } from '@tanstack/react-query';
function useUser(id: number) {
return useQuery({
queryKey: ['user', id],
queryFn: () => fetchJson<User>(`/api/users/${id}`),
});
}
// 在你的组件中
const { data: user, isLoading } = useUser(42);
// user 的类型是 User | undefined
在你的 tsconfig.json 中,启用 strict 模式(至少启用 strictNullChecks):
{
"compilerOptions": {
"strict": true
}
}
没有严格 null 检查,TypeScript 允许你在可能为 null 的值上访问 .username 而不报错。这违背了为 API 响应添加类型的目的。
readonlyAPI 响应是你接收的数据,而不是你应该修改的数据。将属性标记为 readonly:
interface User {
readonly id: number;
readonly username: string;
readonly email: string;
readonly createdAt: string;
}
或对整个接口使用工具类型:
type ImmutableUser = Readonly<User>;
这可以防止导致细微 bug 的意外变更。
使用品牌类型防止混淆不同实体的 ID:
type UserId = number & { readonly __brand: 'UserId' };
type PostId = number & { readonly __brand: 'PostId' };
function getUser(id: UserId): Promise<User> { /* ... */ }
function getPost(id: PostId): Promise<Post> { /* ... */ }
const userId = 42 as UserId;
const postId = 7 as PostId;
getUser(userId); // OK
getUser(postId); // 类型错误!不能将 PostId 传递到期望 UserId 的地方
将所有 API 相关类型保存在专用目录中:
src/
types/
api/
user.ts
post.ts
common.ts // 共享类型如 PaginatedResponse
index.ts // 重新导出
这使类型易于查找、导入和维护。当 API 更改时,你更新一个文件,编译器会显示所有破坏的地方。
永远不要信任外部数据。即使有完美的类型,API 也可能返回意外的内容。在响应进入应用的地方验证它们:
// 不要这样做
const user = (await response.json()) as User; // 盲目信任
// 这样做
const data = await response.json();
const user = UserSchema.parse(data); // 验证形状
类型断言(as User)让编译器安静,但不检查数据。运行时验证(Zod、io-ts、Valibot)才真正验证形状是否匹配。
处理大型或复杂的 JSON 有效负载时,在编写类型之前使用 JSON 编辑器 来探索和理解结构。在合适的编辑器中查看嵌套关系比在纯文本中容易得多。
对于使用 JSON API 的团队,请查看以下相关指南:
如果你的 API 有 OpenAPI(Swagger)规范,完全跳过 JSON 转换。openapi-typescript 等工具直接从规范生成类型:
npx openapi-typescript https://api.example.com/openapi.json -o ./src/types/api.ts
这种方法更优越,因为规范明确定义了可选字段、可空类型和枚举。你无需从示例数据猜测就能获得准确的类型。
处理在浏览器中进行。保存和分享均为可选操作,需要账户,并会将所选内容发送到我们的服务。
是的。转换器为每个嵌套对象生成单独的接口并正确引用它们。
生成的 TypeScript 类型可直接用于生产环境。复制它们或下载为 .ts 文件。
是的。转换器检测数组类型并生成正确的 TypeScript 数组语法(Type[])。当值可以为 null 或 undefined 时,会检测到可选字段。