从 JSON 生成 TypeScript 类型可以节省数小时的手动输入,在编译时捕获 bug,并保持代码库与 API 同步。使用我们的免费在线工具或 quicktype、json2ts 等库自动生成接口。结合 Zod 进行运行时验证。始终优先使用 interface 定义对象形状,并在 tsconfig.json 中启用 strict 模式以获得最大类型安全性。
- 从 JSON 生成的 TypeScript 接口可以在编译时捕获形状不匹配,防止运行时错误。
- 使用 interface 定义对象形状(可扩展、可合并),使用 type 定义联合类型、交叉类型和映射类型。
- 当字段可能缺失时用 ? 标记为可选,当值可能为 null 时使用联合类型 (string | null)。
- quicktype、json2ts 和我们的在线转换器可自动处理嵌套对象、数组和联合类型。
- Zod 通过 z.infer<typeof schema> 在编译时类型和运行时验证之间架起桥梁。
- 在 tsconfig.json 中启用 resolveJsonModule 和 strict 模式,实现完整类型安全的 .json 文件导入。
- 将 API 响应包装在通用的 ApiResponse<T> 类型中,实现全应用统一的错误处理。
试试我们的免费 JSON 转 TypeScript 转换器 →
为什么要从 JSON 生成 TypeScript 类型?
每个现代 Web 应用都与返回 JSON 数据的 API 通信。没有 TypeScript 类型,你就是在盲飞:属性名可能拼写错误,嵌套对象可能为 null,数组可能包含意外的形状。
为复杂的 API 响应手动编写接口既乏味又容易出错。自动生成确保类型与实际数据形状匹配。
从 JSON 生成 TypeScript 类型的主要优势:
- 编译时安全:访问不存在的属性?TypeScript 在代码运行前就能捕获。
- IDE 自动补全:生成的接口为每个嵌套字段提供完整的 IntelliSense。
- 重构信心:重命名类型中的字段,TypeScript 会标记所有需要更新的使用。
- 文档化:类型作为 API 契约的活文档。
- 团队協作:共享类型确保前后端开发者对数据形状达成一致。
// Without types - runtime errors waiting to happen
const user = await fetch("/api/user").then(r => r.json());
console.log(user.nme); // Typo! No error until runtime
console.log(user.address.city); // Crashes if address is null
// With generated types - caught at compile time
interface User {
name: string;
email: string;
address: Address | null;
}
const user: User = await fetch("/api/user").then(r => r.json());
console.log(user.nme); // TS Error: Property 'nme' does not exist
console.log(user.address.city); // TS Error: 'address' is possibly nullinterface 与 type 别名:哪个更适合 JSON 数据?
TypeScript 提供两种定义对象形状的方式:interface 和 type。对于 JSON 派生类型,通常优先使用 interface:
何时使用 interface
interface 是对象形状的默认选择。它支持声明合并和 extends 继承。大多数 JSON 转 TypeScript 工具默认生成 interface。
// Interface: extendable and mergeable
interface User {
id: number;
name: string;
email: string;
}
// Extend an interface
interface AdminUser extends User {
role: "admin";
permissions: string[];
}
// Declaration merging: add fields to existing interface
interface User {
avatar?: string; // Merged with the original User interface
}
// Now User has: id, name, email, avatar何时使用 type 别名
type 别名更适合联合类型、交叉类型、映射类型和条件类型。
// Type alias: ideal for unions and intersections
type Status = "active" | "inactive" | "pending";
type ID = string | number;
// Intersection type
type UserWithPosts = User & { posts: Post[] };
// Mapped type (only possible with type)
type ReadonlyUser = Readonly<User>;
type PartialUser = Partial<User>;
type UserKeys = keyof User; // "id" | "name" | "email"
// Conditional type
type ApiResult<T> = T extends Error ? ErrorResponse : SuccessResponse<T>;经验法则:API 响应对象用 interface,联合类型和工具类型用 type。
处理可选和可空字段
真实世界的 JSON 数据经常有时存在时缺失的字段,或值可能为 null 的字段。TypeScript 区分这些情况:
可选字段 (?)
当字段可能完全不存在于 JSON 中时,使用 ? 修饰符。
// JSON may or may not have "middleName" and "phone"
// { "name": "Alice", "email": "alice@example.com" }
// { "name": "Bob", "email": "bob@example.com", "middleName": "J.", "phone": "+1234" }
interface User {
name: string;
email: string;
middleName?: string; // May be absent from JSON
phone?: string; // May be absent from JSON
}
// Usage: must check before accessing
function getDisplayName(user: User): string {
if (user.middleName) {
return `${user.name} ${user.middleName}`;
}
return user.name;
}可空字段 (| null)
当字段始终存在但值可能为 null 时,使用 null 联合类型。
// JSON always has "bio" but it can be null
// { "name": "Alice", "bio": "Software engineer" }
// { "name": "Bob", "bio": null }
interface UserProfile {
name: string;
bio: string | null; // Always present, may be null
avatarUrl: string | null; // Always present, may be null
}
// Usage: narrow the type before using
function renderBio(profile: UserProfile): string {
return profile.bio ?? "No bio provided";
}既可选又可空
某些 API 返回的字段既可选又可空。组合 ? 和 | null:
interface UserSettings {
theme: "light" | "dark";
nickname?: string | null; // May be absent OR present as null
customColor?: string | null; // May be absent OR present as null
}
// All of these are valid:
const a: UserSettings = { theme: "dark" }; // nickname absent
const b: UserSettings = { theme: "dark", nickname: null }; // nickname is null
const c: UserSettings = { theme: "dark", nickname: "CoolUser" }; // nickname has value嵌套对象和数组
API 返回的 JSON 通常包含深层嵌套结构。TypeScript 通过嵌套接口定义处理这些:
嵌套对象接口
将每个嵌套对象提取为独立的接口,提高可复用性:
// JSON with nested structure:
// {
// "id": 1,
// "name": "Alice",
// "address": { "street": "123 Main", "city": "NYC", "geo": { "lat": 40.7, "lng": -74.0 } },
// "company": { "name": "Acme", "department": "Engineering" }
// }
interface GeoLocation {
lat: number;
lng: number;
}
interface Address {
street: string;
city: string;
state?: string;
zipCode?: string;
geo: GeoLocation;
}
interface Company {
name: string;
department: string;
}
interface User {
id: number;
name: string;
address: Address;
company: Company;
}类型化数组
JSON 中的数组应使用元素类型。使用 Type[] 或 Array<Type> 语法:
interface BlogPost {
id: number;
title: string;
tags: string[]; // Array of primitives
comments: Comment[]; // Array of objects
images: Image[]; // Array of objects
}
interface Comment {
id: number;
author: string;
body: string;
replies: Comment[]; // Recursive type: comments can have replies
}
interface Image {
url: string;
alt: string;
width: number;
height: number;
}
// Type[] vs Array<Type> - both are equivalent
type StringArray = string[];
type StringArray2 = Array<string>;
// ReadonlyArray prevents push/pop/splice
type ImmutableTags = ReadonlyArray<string>;固定长度数组的元组类型
如果 JSON 包含固定数量特定类型元素的数组,使用元组类型:
// JSON: { "coordinates": [40.7128, -74.0060] }
// JSON: { "range": [1, 100] }
type Coordinates = [number, number]; // [latitude, longitude]
type Range = [number, number]; // [min, max]
type RGB = [number, number, number]; // [red, green, blue]
type NameAge = [string, number]; // ["Alice", 30]
interface Location {
name: string;
coordinates: Coordinates;
}
// Labeled tuple elements (TypeScript 4.0+)
type Point = [x: number, y: number, z?: number];从不同 JSON 值生成联合类型
某些 JSON 字段可以包含不同类型的值。TypeScript 联合类型可以自然地处理这些:
字符串字面量联合
当字段有固定的可能值集时,使用字符串字面量联合:
// String literal union for status field
type OrderStatus = "pending" | "processing" | "shipped" | "delivered" | "cancelled";
interface Order {
id: string;
status: OrderStatus; // Only these 5 values allowed
priority: "low" | "medium" | "high";
}
// Number literal union
type HttpStatus = 200 | 201 | 400 | 401 | 403 | 404 | 500;
// Mixed type union
type ID = string | number; // API returns ID as string or number
interface Product {
id: ID;
price: number;
discount: number | null; // null means no discount
}可辨别联合
当 JSON 对象有一个 "type" 字段决定其形状时,使用可辨别联合:
// Discriminated union: "type" field determines the shape
interface TextMessage {
type: "text";
content: string;
}
interface ImageMessage {
type: "image";
url: string;
width: number;
height: number;
}
interface VideoMessage {
type: "video";
url: string;
duration: number;
thumbnail: string;
}
type Message = TextMessage | ImageMessage | VideoMessage;
// TypeScript narrows the type based on the discriminant
function renderMessage(msg: Message) {
switch (msg.type) {
case "text":
return msg.content; // TypeScript knows this is TextMessage
case "image":
return msg.url; // TypeScript knows this is ImageMessage
case "video":
return msg.thumbnail; // TypeScript knows this is VideoMessage
}
}试试我们的免费 JSON 转 TypeScript 转换器 →
JSON 转 TypeScript 生成工具
多种工具可以自动从 JSON 生成 TypeScript 接口:
quicktype
Quicktype 是最强大的 JSON 转 TypeScript 工具。支持联合类型推断、可选字段和多种输出语言:
# Install quicktype CLI
npm install -g quicktype
# Generate TypeScript from a JSON file
quicktype -s json -o types.ts --lang ts data.json
# Generate from a JSON URL (API endpoint)
quicktype -s json -o types.ts --lang ts "https://api.example.com/users"
# Generate with runtime type checks (io-ts)
quicktype -s json -o types.ts --lang ts --just-types data.json
# Generate interfaces (not classes)
quicktype -s json -o types.ts --lang ts --just-types --interfaces-only data.json
# Add to package.json scripts
# "generate:types": "quicktype -s json -o src/types/api.ts --lang ts --just-types api-response.json"json2ts / json-to-ts
一个更简单的替代方案,可作为 npm 包和 VS Code 扩展使用:
# Install json-to-ts
npm install -g json-to-ts
# Use programmatically in Node.js
const JsonToTS = require("json-to-ts");
const json = {
id: 1,
name: "Alice",
email: "alice@example.com",
address: {
street: "123 Main St",
city: "New York"
},
tags: ["developer", "designer"]
};
const interfaces = JsonToTS(json);
console.log(interfaces.join("\n\n"));
// interface RootObject {
// id: number;
// name: string;
// email: string;
// address: Address;
// tags: string[];
// }
//
// interface Address {
// street: string;
// city: string;
// }在线转换器
对于快速的一次性转换,我们的免费在线工具让您粘贴 JSON 即可获得 TypeScript 接口:
VS Code 扩展
多个 VS Code 扩展可以直接在编辑器中将 JSON 转换为 TypeScript。
Zod:带 TypeScript 类型的运行时验证
TypeScript 类型仅存在于编译时。Zod 通过提供同时生成 TypeScript 类型的运行时验证来解决这个问题:
基本 Zod Schema
定义 schema 并从中推断 TypeScript 类型:
import { z } from "zod";
// Define a schema
const UserSchema = z.object({
id: z.number(),
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().positive().optional(),
role: z.enum(["admin", "user", "moderator"]),
address: z.object({
street: z.string(),
city: z.string(),
zipCode: z.string().regex(/^\d{5}(-\d{4})?$/),
}).nullable(),
});
// Infer the TypeScript type from the schema
type User = z.infer<typeof UserSchema>;
// Equivalent to:
// interface User {
// id: number;
// name: string;
// email: string;
// age?: number | undefined;
// role: "admin" | "user" | "moderator";
// address: { street: string; city: string; zipCode: string } | null;
// }
// Validate data at runtime
const result = UserSchema.safeParse(jsonData);
if (result.success) {
const user = result.data; // Fully typed as User
} else {
console.error(result.error.issues);
}用 Zod 验证 API 响应
使用 Zod 在使用数据前验证 fetch 响应:
import { z } from "zod";
const ProductSchema = z.object({
id: z.string().uuid(),
name: z.string(),
price: z.number().positive(),
inStock: z.boolean(),
categories: z.array(z.string()),
});
type Product = z.infer<typeof ProductSchema>;
async function fetchProduct(id: string): Promise<Product> {
const response = await fetch(`/api/products/${id}`);
const json = await response.json();
// Validate at runtime - throws if shape is wrong
return ProductSchema.parse(json);
}
// Safe version that does not throw
async function fetchProductSafe(id: string) {
const response = await fetch(`/api/products/${id}`);
const json = await response.json();
const result = ProductSchema.safeParse(json);
if (!result.success) {
console.error("API response validation failed:", result.error.flatten());
return null;
}
return result.data;
}Zod 转换
Zod 可以在验证过程中转换数据:
const EventSchema = z.object({
id: z.number(),
title: z.string().trim(), // Auto-trim whitespace
// Transform ISO string to Date object
startDate: z.string().transform((s) => new Date(s)),
endDate: z.string().transform((s) => new Date(s)),
// Default value if missing
maxAttendees: z.number().default(100),
// Coerce string to number (common in form data / query params)
ticketPrice: z.coerce.number(),
});
// Input type (what the API returns)
type EventInput = z.input<typeof EventSchema>;
// { id: number; title: string; startDate: string; endDate: string; ... }
// Output type (what you get after parsing)
type Event = z.output<typeof EventSchema>;
// { id: number; title: string; startDate: Date; endDate: Date; ... }通用 API 响应类型
大多数 API 将数据包装在标准信封中。定义泛型类型来统一处理:
基本泛型响应
泛型 ApiResponse<T> 让您对任何数据类型复用相同的包装:
// Generic API response wrapper
interface ApiResponse<T> {
success: boolean;
data: T;
error?: {
code: string;
message: string;
details?: Record<string, string[]>;
};
meta?: {
requestId: string;
timestamp: string;
};
}
// Usage with specific types
type UserResponse = ApiResponse<User>;
type ProductListResponse = ApiResponse<Product[]>;
type DeleteResponse = ApiResponse<null>;分页响应
为分页端点扩展泛型模式:
interface PaginationMeta {
page: number;
perPage: number;
total: number;
totalPages: number;
hasNextPage: boolean;
hasPrevPage: boolean;
}
interface PaginatedResponse<T> {
success: boolean;
data: T[];
pagination: PaginationMeta;
}
// Usage
type PaginatedUsers = PaginatedResponse<User>;
type PaginatedProducts = PaginatedResponse<Product>;
// Example response shape:
// {
// "success": true,
// "data": [{ "id": 1, "name": "Alice" }, ...],
// "pagination": { "page": 1, "perPage": 20, "total": 150, ... }
// }使用泛型响应类型
将泛型响应类型与 fetch 函数结合实现端到端类型安全:
// Typed API client using generic response types
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async get<T>(path: string): Promise<ApiResponse<T>> {
const res = await fetch(`${this.baseUrl}${path}`);
return res.json() as Promise<ApiResponse<T>>;
}
async getPaginated<T>(path: string, page = 1): Promise<PaginatedResponse<T>> {
const res = await fetch(`${this.baseUrl}${path}?page=${page}`);
return res.json() as Promise<PaginatedResponse<T>>;
}
}
// Usage - fully typed
const api = new ApiClient("https://api.example.com");
const userRes = await api.get<User>("/users/1");
// userRes.data is typed as User
const productsRes = await api.getPaginated<Product>("/products");
// productsRes.data is typed as Product[]
// productsRes.pagination is typed as PaginationMeta在 TypeScript 中使用 JSON API(fetch + 类型安全)
内置的 fetch API 返回未类型化的 .json()。以下是添加类型安全的方法:
类型化 Fetch 包装器
创建一个泛型 fetch 包装器:
// Type-safe fetch wrapper
async function fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
const response = await fetch(url, {
...init,
headers: {
"Content-Type": "application/json",
...init?.headers,
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json() as Promise<T>;
}
// Usage
const user = await fetchJson<User>("/api/users/1");
// user is typed as User
const products = await fetchJson<Product[]>("/api/products");
// products is typed as Product[]
// POST with typed body
const newUser = await fetchJson<User>("/api/users", {
method: "POST",
body: JSON.stringify({ name: "Alice", email: "alice@test.com" }),
});Axios 与 TypeScript
Axios 开箱支持泛型:
import axios from "axios";
// Axios supports generics for response typing
const { data: user } = await axios.get<User>("/api/users/1");
// user is typed as User
const { data: products } = await axios.get<Product[]>("/api/products");
// products is typed as Product[]
// POST with typed request and response
interface CreateUserRequest {
name: string;
email: string;
}
const { data: created } = await axios.post<User, { data: User }, CreateUserRequest>(
"/api/users",
{ name: "Alice", email: "alice@test.com" }
);类型安全的错误处理
定义错误类型并使用可辨别联合:
// Discriminated union for API results
type Result<T, E = Error> =
| { ok: true; data: T }
| { ok: false; error: E };
interface ApiError {
code: string;
message: string;
statusCode: number;
}
async function fetchSafe<T>(url: string): Promise<Result<T, ApiError>> {
try {
const response = await fetch(url);
if (!response.ok) {
return {
ok: false,
error: {
code: "HTTP_ERROR",
message: response.statusText,
statusCode: response.status,
},
};
}
const data = (await response.json()) as T;
return { ok: true, data };
} catch (err) {
return {
ok: false,
error: {
code: "NETWORK_ERROR",
message: err instanceof Error ? err.message : "Unknown error",
statusCode: 0,
},
};
}
}
// Usage: TypeScript narrows based on ok field
const result = await fetchSafe<User>("/api/users/1");
if (result.ok) {
console.log(result.data.name); // Typed as User
} else {
console.error(result.error.code); // Typed as ApiError
}试试我们的免费 JSON 转 TypeScript 转换器 →
tsconfig strict 模式和 JSON 导入
tsconfig.json 设置显著影响 TypeScript 处理 JSON 数据的方式:
启用 Strict 模式
strict 模式启用一组类型检查选项。对于 JSON 项目,strictNullChecks 尤其重要:
// tsconfig.json - recommended for JSON-heavy projects
{
"compilerOptions": {
"strict": true,
// strict enables all of these:
// "strictNullChecks": true, // Forces null/undefined handling
// "strictFunctionTypes": true, // Stricter function type checking
// "strictBindCallApply": true, // Stricter bind/call/apply
// "strictPropertyInitialization": true,
// "noImplicitAny": true, // No implicit any types
// "noImplicitThis": true,
// "alwaysStrict": true,
// Additional recommended options
"noUncheckedIndexedAccess": true, // obj[key] returns T | undefined
"noPropertyAccessFromIndexSignature": true,
"exactOptionalPropertyTypes": true
}
}
// With strictNullChecks, this is an error:
const user: User = await fetchJson("/api/users/1");
console.log(user.address.city);
// ~~~~~~~~~~~~~ Error: Object is possibly 'null'
// Must handle null:
console.log(user.address?.city ?? "Unknown");导入 JSON 文件
启用 resolveJsonModule 后,TypeScript 可以导入 .json 文件并自动推断类型:
// tsconfig.json
{
"compilerOptions": {
"resolveJsonModule": true, // Enable JSON imports
"esModuleInterop": true // Enable default imports
}
}
// Now you can import JSON files with full type inference
import config from "./config.json";
// TypeScript infers: { port: number; host: string; debug: boolean }
import countries from "./countries.json";
// TypeScript infers the full shape of the JSON
// The imported values are fully typed
console.log(config.port); // number
console.log(config.host); // string
console.log(config.unknown); // Error: Property 'unknown' does not existesModuleInterop
启用 esModuleInterop 以获得更整洁的 CommonJS 模块导入:
// Without esModuleInterop:
import * as fs from "fs"; // Namespace import required
const data = JSON.parse(fs.readFileSync("data.json", "utf-8"));
// With esModuleInterop:
import fs from "fs"; // Clean default import
const data = JSON.parse(fs.readFileSync("data.json", "utf-8"));
// Works for JSON-related libraries too:
import Ajv from "ajv"; // JSON Schema validator
import yaml from "js-yaml"; // YAML parser
import csvParser from "csv-parser"; // CSV parser维护生成类型的最佳实践
自动生成的类型需要持续维护以与 API 保持同步:
- 自动化生成:在 package.json 中添加从 API Schema 重新生成类型的脚本。
- 版本控制类型:将生成的类型提交到版本控制。
- 分离生成与手写:生成类型放在
types/generated/,手写类型放在types/。 - 使用 readonly:用
Readonly<T>标记生成类型以防止意外修改。 - 添加 JSDoc 注释:用 API 文档中的描述记录字段。
- 在边界验证:在 API 边界使用 Zod 或 io-ts 捕获运行时不匹配。
- CI 类型检查:在 CI 管道中添加
tsc --noEmit。 - 保持类型 DRY:使用 TypeScript 工具类型(Pick, Omit, Partial)派生子类型。
// Example: project structure for generated types
src/
types/
generated/
api-users.ts // Auto-generated from OpenAPI spec
api-products.ts // Auto-generated from JSON samples
index.ts // Re-exports all types
utils.ts // Hand-written utility types
// package.json scripts
{
"scripts": {
"generate:types": "quicktype -s schema api/openapi.yaml -o src/types/generated/api.ts --lang ts --just-types",
"typecheck": "tsc --noEmit",
"prebuild": "npm run generate:types && npm run typecheck"
}
}
// Use Readonly to prevent mutation of API data
type ReadonlyUser = Readonly<User>;
type DeepReadonlyUser = {
readonly [K in keyof User]: User[K] extends object
? Readonly<User[K]>
: User[K];
};
// Use utility types to derive sub-types
type CreateUserPayload = Omit<User, "id" | "createdAt">;
type UpdateUserPayload = Partial<Omit<User, "id">>;
type UserSummary = Pick<User, "id" | "name" | "email">;常见问题
如何在线将 JSON 转换为 TypeScript 接口?
将 JSON 粘贴到我们的免费在线 JSON 转 TypeScript 转换器工具。它会自动生成带有嵌套、可选字段和数组类型的 TypeScript 接口。
TypeScript 中 JSON 数据应该用 interface 还是 type?
大多数 JSON 对象形状使用 interface。interface 支持声明合并和 extends。联合类型和工具类型使用 type。
如何在 TypeScript 中处理 JSON 的可选和可空字段?
可能缺失的字段用 ?,值可能为 null 的字段用 | null,两者都有则组合使用 ?: string | null。
自动生成 TypeScript 类型的最佳工具是什么?
quicktype 功能最强大,支持联合类型推断和多语言输出。简单需求可用 json-to-ts 或 VS Code 扩展。
Zod 如何帮助 JSON 和 TypeScript?
Zod 提供运行时验证 schema,同时通过 z.infer<typeof schema> 生成 TypeScript 类型。一次定义,同时获得编译时和运行时保护。
如何在 TypeScript 中安全地导入 JSON 文件?
在 tsconfig.json 中启用 resolveJsonModule 和 esModuleInterop,然后直接 import data from "./data.json"。
JSON 项目的重要 tsconfig 设置有哪些?
启用 strict、resolveJsonModule、esModuleInterop 和 noUncheckedIndexedAccess。
如何在 TypeScript 中创建通用 API 响应类型?
定义泛型接口:interface ApiResponse<T> { success: boolean; data: T; error?: string; }。然后用具体类型使用:ApiResponse<User>。
总结
从 JSON 生成 TypeScript 类型是现代 Web 开发中投入产出比最高的实践之一。它在编译时捕获 bug,提供 IDE 自动补全,并保持前端与后端 API 同步。从我们的免费在线工具开始快速转换,在 CI 管道中采用 quicktype,并在 API 边界添加 Zod 进行运行时验证。