跳到主要內容

TypeScript 實用技巧:10 個讓你的 AI 生成代碼更可靠的型別寫法

你叫 AI 寫一個函式,它寫出來了,但型別全是 any。或者它用了 as 強制轉型,編譯通過了,runtime 卻爆掉。

問題不在 AI 不夠聰明,而是你的型別沒有給它足夠的資訊。TypeScript 的型別系統不只是給編譯器看的——它是你和 AI 之間的合約。型別越精確,AI 生成的 code 越可靠。

以下 10 個技巧,每個都附前後對比。

1. 用 satisfies 取代 as

// 之前:as 會吞掉型別錯誤
const config = {
  port: 3000,
  host: 'localhost',
  debug: 'yes', // 應該是 boolean,但 as 不會報錯
} as Config

// 之後:satisfies 檢查型別但保留推斷
const config = {
  port: 3000,
  host: 'localhost',
  debug: true,
} satisfies Config
// config.port 的型別是 3000(literal),不是 number

as 是告訴 TypeScript「我比你懂」,它會跳過檢查。satisfies 是「幫我檢查,但不要拓寬型別」。AI 生成 config 物件時,satisfies 能在編譯期就抓到錯誤。

2. const assertion

// 之前:型別被拓寬
const routes = ['/', '/about', '/blog']
// 型別:string[]

// 之後:保留 literal types
const routes = ['/', '/about', '/blog'] as const
// 型別:readonly ['/', '/about', '/blog']

as const 讓陣列和物件的值變成 literal type。當你把 routes 傳給接受特定字串的函式時,TypeScript 知道裡面只有這三個值,AI 也能根據這個資訊生成更精確的 code。

3. Template literal types

// 之前:手動列舉所有可能
type EventName = 'click_button' | 'click_link' | 'hover_button' | 'hover_link'

// 之後:用 template literal 組合
type Action = 'click' | 'hover' | 'focus'
type Target = 'button' | 'link' | 'input'
type EventName = `${Action}_${Target}`
// 自動產生所有 3×3 = 9 種組合

當你的字串有規律的命名模式時,template literal types 比手動列舉更不容易遺漏。AI 在生成 event handler 時,能自動推斷出所有合法的 event name。

4. Discriminated unions 取代 enum

// 之前:用 enum + 條件判斷
enum Status {
  Loading,
  Success,
  Error,
}
interface State {
  status: Status
  data?: string
  error?: Error
}
// data 和 error 永遠是 optional,容易忘記檢查

// 之後:discriminated union
type State =
  | { status: 'loading' }
  | { status: 'success'; data: string }
  | { status: 'error'; error: Error }

function handle(state: State) {
  switch (state.status) {
    case 'success':
      console.log(state.data) // TypeScript 知道 data 一定存在
      break
    case 'error':
      console.log(state.error) // TypeScript 知道 error 一定存在
      break
  }
}

Discriminated union 讓每個 status 對應精確的 payload。AI 在 switch case 裡不會忘記處理某個狀態,因為 TypeScript 會用 exhaustiveness check 提醒它。

5. Branded types 防止 ID 混用

// 之前:所有 ID 都是 string,容易傳錯
function getUser(userId: string) { ... }
function getOrder(orderId: string) { ... }
getUser(orderId) // 不會報錯,但邏輯完全錯

// 之後:branded types
type UserId = string & { __brand: 'UserId' }
type OrderId = string & { __brand: 'OrderId' }

function userId(id: string): UserId { return id as UserId }
function orderId(id: string): OrderId { return id as OrderId }

function getUser(id: UserId) { ... }
function getOrder(id: OrderId) { ... }
getUser(orderId('abc')) // 編譯錯誤

Branded types 在 runtime 沒有成本(__brand 不存在於實際值),但在型別層面區分了不同意義的 string。AI 生成 API call 時不會把 orderId 傳進 getUser。

6. Utility types 的正確時機

// Partial:建立 update payload(所有欄位選填)
type UpdateUser = Partial<User>

// Required:強制所有欄位必填(例如 form 驗證後)
type ValidatedUser = Required<User>

// Pick:只取需要的欄位
type UserPreview = Pick<User, 'id' | 'name' | 'avatar'>

// Omit:排除敏感欄位
type PublicUser = Omit<User, 'password' | 'email'>

原則:Pick 用在「我只要這幾個」,Omit 用在「我全都要,但排除這幾個」。欄位少的時候用 Pick,排除少的時候用 Omit。告訴 AI 你的 API response 長什麼樣,它就能生成正確的 data fetching code。

7. infer 關鍵字

// 從函式的回傳值推斷型別
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never

// 實際用法:從 API handler 推斷 response 型別
type ApiResponse = ReturnOf<typeof getUserHandler>

// 從 Promise 裡解包
type Unwrap<T> = T extends Promise<infer U> ? U : T
type User = Unwrap<Promise<{ id: string; name: string }>>
// 型別:{ id: string; name: string }

infer 讓你在 conditional type 裡「捕捉」某個位置的型別。當你的 API 層已經有完整的函式定義時,用 infer 自動推斷回傳型別,比手寫 interface 更不容易出錯。

8. Function overloads

// 之前:union type 讓回傳值不精確
function parse(input: string | number): string | number { ... }

// 之後:overload 讓每個 input 對應精確的 output
function parse(input: string): number
function parse(input: number): string
function parse(input: string | number): string | number {
  if (typeof input === 'string') return parseInt(input, 10)
  return String(input)
}

const a = parse('42')  // 型別:number
const b = parse(42)    // 型別:string

Overloads 讓同一個函式根據 input 型別給出不同的 output 型別。AI 在呼叫 overloaded function 時,能根據傳入的參數自動推斷回傳值,不需要手動 assert。

9. Zod 做 runtime validation

import { z } from 'zod'

// 定義 schema(同時是 runtime validator 和 type)
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
  role: z.enum(['admin', 'member', 'guest']),
})

// 自動推斷 TypeScript 型別
type User = z.infer<typeof UserSchema>

// runtime 驗證(API 邊界、form input)
const result = UserSchema.safeParse(apiResponse)
if (result.success) {
  console.log(result.data.name) // 型別安全
} else {
  console.log(result.error.issues) // 結構化的錯誤資訊
}

TypeScript 只在編譯期檢查,外部資料(API response、user input、webhook payload)需要 runtime validation。Zod 讓你用一份 schema 同時產生型別和 validator,AI 生成的 API integration code 如果用 Zod,就不會漏掉邊界檢查。

10. 型別測試

// 用 @ts-expect-error 做型別層面的斷言
type Assert<T, Expected> = T extends Expected ? true : never

// 確認型別推斷正確
type _test1 = Assert<ReturnOf<typeof getUser>, Promise<User>> // 通過
// @ts-expect-error — UserId 不能指派給 OrderId
const _test2: OrderId = userId('abc') // 這行「應該」報錯

型別測試確保你的 utility types 和 generic functions 在重構後仍然正確。如果你改了 User 型別,@ts-expect-error 會在預期的錯誤消失時提醒你——代表某個型別約束被意外放寬了。

給 AI 更好的型別,拿到更好的 code

這 10 個技巧的共同點:它們都在增加型別的資訊量。satisfiesas 多了檢查,branded types 比 plain string 多了語意,discriminated union 比 optional fields 多了狀態關聯。

AI 模型讀你的 codebase 時,型別就是它最重要的上下文。型別越精確,它生成的 code 就越不需要你手動修正。

投資在型別上的時間,會在每次 AI 生成 code 時回本。


想系統性地學習 TypeScript 在 AI 開發中的應用?到 Docs 查看完整的技術學習路徑。