轉換器根據樣本值產生初稿。以下手動調整需要了解 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 時,會偵測到可選欄位。