技術 6 分鐘閱讀
TypeScript 實用技巧:寫出更安全的程式碼
前言
TypeScript 已經是現代前端開發的標配。但很多人只是在型別上加個 any 就交差了,沒有真正發揮 TypeScript 的威力。這篇文章分享一些我在實戰中常用的技巧。
善用型別推導
不要過度標註型別
TypeScript 的型別推導很強大,很多時候不需要手動標註:
// 不需要:TypeScript 會自動推導
const name: string = '小明';
const age: number = 25;
const items: string[] = ['a', 'b', 'c'];
// 這樣就好
const name = '小明';
const age = 25;
const items = ['a', 'b', 'c'];
只在推導結果不符合預期時才手動標註。
as const 讓型別更精確
// type: string[]
const roles = ['admin', 'editor', 'viewer'];
// type: readonly ['admin', 'editor', 'viewer']
const roles = ['admin', 'editor', 'viewer'] as const;
// 從 as const 陣列建立聯合型別
type Role = typeof roles[number]; // 'admin' | 'editor' | 'viewer'
實用的工具型別
Partial 與 Required
interface User {
id: number;
name: string;
email: string;
avatar?: string;
}
// 所有欄位變成可選(適合更新操作)
type UpdateUserDTO = Partial<User>;
// 所有欄位變成必填
type CompleteUser = Required<User>;
Pick 與 Omit
// 只取需要的欄位
type UserPreview = Pick<User, 'id' | 'name' | 'avatar'>;
// 排除不需要的欄位
type CreateUserDTO = Omit<User, 'id'>;
Record
// 定義鍵值對的型別
type StatusMap = Record<string, {
label: string;
color: string;
}>;
const orderStatus: StatusMap = {
pending: { label: '待處理', color: 'yellow' },
completed: { label: '已完成', color: 'green' },
cancelled: { label: '已取消', color: 'red' },
};
泛型的實際應用
API 回應的泛型包裝
interface ApiResponse<T> {
success: boolean;
data: T;
message: string;
}
interface PaginatedResponse<T> {
data: T[];
meta: {
currentPage: number;
lastPage: number;
perPage: number;
total: number;
};
}
// 使用
async function fetchProducts(): Promise<ApiResponse<PaginatedResponse<Product>>> {
const response = await fetch('/api/products');
return response.json();
}
泛型元件
interface SelectProps<T> {
options: T[];
value: T | null;
onChange: (value: T) => void;
getLabel: (item: T) => string;
getValue: (item: T) => string | number;
}
function Select<T>({ options, value, onChange, getLabel, getValue }: SelectProps<T>) {
return (
<select
value={value ? String(getValue(value)) : ''}
onChange={(e) => {
const selected = options.find(
(o) => String(getValue(o)) === e.target.value
);
if (selected) onChange(selected);
}}
>
<option value="">請選擇</option>
{options.map((option) => (
<option key={String(getValue(option))} value={String(getValue(option))}>
{getLabel(option)}
</option>
))}
</select>
);
}
// 使用時自動推導型別
<Select
options={categories}
value={selectedCategory}
onChange={setSelectedCategory}
getLabel={(c) => c.name} // c 自動推導為 Category
getValue={(c) => c.id}
/>
Discriminated Union
這是處理多種狀態最優雅的方式:
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
function renderState<T>(state: AsyncState<T>) {
switch (state.status) {
case 'idle':
return <p>尚未開始</p>;
case 'loading':
return <Spinner />;
case 'success':
return <DataView data={state.data} />; // TypeScript 知道 data 存在
case 'error':
return <ErrorMessage message={state.error} />; // TypeScript 知道 error 存在
}
}
型別守衛
自定義型別守衛
interface Admin {
role: 'admin';
permissions: string[];
}
interface Member {
role: 'member';
memberSince: Date;
}
type User = Admin | Member;
function isAdmin(user: User): user is Admin {
return user.role === 'admin';
}
function handleUser(user: User) {
if (isAdmin(user)) {
console.log(user.permissions); // TypeScript 知道是 Admin
} else {
console.log(user.memberSince); // TypeScript 知道是 Member
}
}
Zod 型別驗證
搭配 Zod 做執行時的型別驗證:
import { z } from 'zod';
const ProductSchema = z.object({
id: z.number(),
name: z.string().min(1),
price: z.number().positive(),
category: z.enum(['electronics', 'clothing', 'food']),
});
// 從 Schema 推導出 TypeScript 型別
type Product = z.infer<typeof ProductSchema>;
// 執行時驗證
function processProduct(data: unknown) {
const result = ProductSchema.safeParse(data);
if (result.success) {
// result.data 是型別安全的 Product
console.log(result.data.name);
} else {
console.error(result.error.issues);
}
}
常用的型別技巧
從物件推導型別
const config = {
api: 'https://api.example.com',
timeout: 5000,
retries: 3,
} as const;
type Config = typeof config;
// { readonly api: "https://api.example.com"; readonly timeout: 5000; readonly retries: 3; }
條件型別
type IsString<T> = T extends string ? true : false;
// 更實用的範例:根據參數型別決定回傳型別
function process<T extends string | number>(input: T): T extends string ? string[] : number {
if (typeof input === 'string') {
return input.split(',') as any;
}
return (input * 2) as any;
}
模板字面值型別
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiPath = '/users' | '/products' | '/orders';
type ApiEndpoint = `${HttpMethod} ${ApiPath}`;
// 'GET /users' | 'GET /products' | 'GET /orders' | 'POST /users' | ...
結語
TypeScript 的型別系統非常強大,但不需要一次學完所有功能。從最基本的型別標註開始,慢慢加入泛型和工具型別,你會發現程式碼變得越來越安全也越來越好維護。記住,TypeScript 的目的是幫助你,不是給你添麻煩。如果某個型別寫法讓你痛苦不堪,那可能是方向錯了。
#TypeScript #JavaScript #前端開發 #型別系統