今天不只回顧 30 天的內容,我會把「設計心法 → 專案落地檢查清單 → 升級路線」一次給齊。
你可以拿這篇當 團隊 Onboarding 指南,也能當自己做專案的 Checklist。走起。
一句話總結:
用「單一真相來源」+ 「型別工具鏈」,讓前後端協作、API、表單、事件、錯誤流,都能在編譯期先踩煞車。
資料模型單一真相(SSOT)
型別集中於 src/types/ 或 shared 套件;UI / API / DB 都由它衍生,拒絕重複抄一遍。
窄化優先,any 最後招
typeof、instanceof、in、自訂守衛、Discriminated Union 把不確定消掉;
any 只在邊界(第三方)使用,且立刻轉回安全型別。
泛型要「必要且有關聯」
T, K extends keyof T, T[K]:參數之間要能互相約束。
過度抽象 = 可讀性地獄。
錯誤是型別,不是例外
Result<T, E> + ApiError 搭配 zod,讓成功/失敗「各有自己的型別」,呼叫端不必 everywhere try/catch。
Runtime 驗證永遠要配型別
環境變數、API 入參/回傳、第三方資料 → zod/valibot 驗證,z.infer 同步 TS 型別。
範圍思維:把「字串」變成型別
Template Literal Types + Union:事件名、API 路徑、CSS Class 等「字串規則」→ 型別化。
永遠留一個 never 兜底
assertNever(x: never) 保證分支全面;新增成員時,編譯器會叫醒你。
tsconfig.json(嚴格但好用)json
CopyEdit
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "exactOptionalPropertyTypes": true,
    "noUncheckedIndexedAccess": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "useUnknownInCatchVariables": true,
    "importsNotUsedAsValues": "error"
  }
}
重點:exactOptionalPropertyTypes、noUncheckedIndexedAccess 讓你少踩很多邊角雷。
@typescript-eslint/parser、@typescript-eslint/eslint-plugin
@typescript-eslint/consistent-type-imports
@typescript-eslint/explicit-function-return-type(公共 API 上開)@typescript-eslint/no-explicit-any(允許極少數例外註解)bash
CopyEdit
src/
  types/          # Domain models、API types、Result、Error
  utils/          # 型別工具、守衛
  features/...
shared/           # (mono-repo) 前後端共用的 schema/type
src/env.ts + zod(Day 16)zod.parse 擋垃圾資料zod.parse 保證形狀 + Result 介面化錯誤validate(schema) middleware(Day 17)select 精準挑欄位;include 慎用$transaction 合批行為P2002)→ 對應 ApiError.code
type 或 interface,必要時用 ComponentProps<"button"> 合併原生useState<T | null> 明確 null
React.MouseEvent<HTMLButtonElement> 等zod + react-hook-form(zodResolver)@tanstack/react-query + 泛型(useQuery<User[]>)tsd 或 expectTypeOf(Vitest 4)tsc -p tsconfig.json --noEmit 型別檢查eslint .
vitest run
pnpm dedupe / npm dedupe,減少依賴地雷。allowJs: true(首期)strict、noImplicitAny,掃紅線
types/、抽出 Result、ApiError
allowJs,全量 TS每一步都可獨立合併、回滾風險小。
「值」當「型別」:const path = "/api"; type T = \prefix-${path} ❌ → 必須是字面型別:type Path = "/api";
什麼都 any:短期舒服、後期火葬場
→ 用 unknown + 守衛;邊界 zod
過度泛型:讀不懂 → 重構成具體型別 + 少量泛型
Enum 亂用:多半用 Literal Union("A" | "B")較輕
as 成癮:as 是逃生門,不是正門
→ 先想「如何讓編譯器推論到」,必要時再 as const/satisfies
ts
CopyEdit
type Brand<K, T> = K & { __brand: T };
type UserId = Brand<string, "UserId">;
function getUser(id: UserId) {}
// getUser("abc") ❌ 不能亂傳
const id = "abc" as UserId;
getUser(id); // ✅
satisfies:讓值符合型別、又不丟失更精準的推論ts
CopyEdit
const routes = {
  users: "/api/users",
  posts: "/api/posts",
} as const satisfies Record<string, `/${string}`>;
as const:把值「縮窄成字面量」ts
CopyEdit
const ROLES = ["admin", "user", "guest"] as const;
type Role = typeof ROLES[number]; // "admin" | "user" | "guest"
Pick / Omit / Partial / Required / Readonly
Record<K, T> 生映射表ReturnType<T>、Parameters<T> 從函式抽型別never 兜底ts
CopyEdit
function assertNever(x: never): never {
  throw new Error("Unhandled: " + x);
}
type-fest、ts-toolbelt、zod、valibot、io-ts
ts-reset(修補內建型別)、tsup/tsc -w、tsx(跑 TS)prisma、drizzle
@tanstack/react-query、react-hook-form + @hookform/resolvers/zod
tRPC(端到端型別)、OpenAPI + 代碼產生vitest、tsd、expect-type、E2E:playwright
tsc --noEmit
題目:做一個「型別驅動的部落格 CMS」
types/(User/Post DTO、ApiError、Result)tsc --noEmit、eslint、vitest run
交付物:README 截圖、/docs/ 放型別設計圖(自豪地展示你的 SSOT)。
30 天走到這裡,你不只「會用 TS」,你已經具備把 TS 落地到專案的能力。
接下來的功課只有一個:一直用它,讓型別變成你每天省時間的武器。