技術 6 分鐘閱讀

TypeScript 實用技巧:寫出更安全的程式碼

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 #前端開發 #型別系統
Thousand Lab

Thousand Lab