今天不只回顧 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 落地到專案的能力。
接下來的功課只有一個:一直用它,讓型別變成你每天省時間的武器。