免費線上書,16 章:https://www.totaltypescript.com/books/total-typescript-essentials
📌 30 秒摘要(Layer 3|讀完用自己的話寫一句)
這本書的一句話:TypeScript 的功力不在於「會不會寫型別」,而在於知道什麼時候該讓 TS 自己推論、什麼時候該出手約束——書裡幾乎每一章都在處理這條界線。
🗺 心智圖(Canvas)
Total TypeScript Essentials
Link to original 📘 Total TypeScript Essentials
🧩 型別塑形
🎯 什麼情境該想到我
當你的型別變成一個「選配欄位袋(bag of optionals)」——一堆
?:欄位,而且沒人說得清哪些欄位會同時出現的時候。⚙️ 怎麼用(步驟 / 公式)
病徵:太鬆的型別
type State = { status: 'loading' | 'success' | 'error' error?: string data?: string }問題有兩層:
- 存取時型別不準——判斷完
status === 'error',state.error依然是string | undefined。- 能造出不可能發生的值——
{ status: 'loading', error: '...', data: '...' }完全合法。解法:把欄位綁到它真正屬於的那個狀態
type State = | { status: 'loading' } | { status: 'error'; error: string } | { status: 'success'; data: string }
- 選一個判別欄位(discriminant):一個字面量型別、且每個成員都不同的共同屬性(這裡是
status)。- 把每個狀態拆成獨立的物件型別。
- 只把該狀態真正需要的資料放進去。
現在判斷完
status === 'error',TS 的**收窄(narrowing)**會讓state.error直接是string,不用再檢查 undefined。
成員多了可以各自抽成具名 type alias 再組成聯集,維持可讀性。心法
讓不可能的狀態變成編不過的程式碼,而不是靠註解或紀律去記「loading 的時候不要看 error」。
4. ★ 窮盡性檢查:讓「漏處理」變成編譯錯誤
function area(s: Shape): number { switch (s.kind) { case "circle": return Math.PI * s.r ** 2; case "square": return s.side ** 2; default: { // ★ 加了新的 kind 卻忘了處理 → s 不再是 never → 這一行編譯錯誤 const _exhaustive: never = s; throw new Error(`unhandled shape: ${JSON.stringify(_exhaustive)}`); } } }★ 這是 TS 相對 Go 的一個實質優勢。
Go 的switch漏一個 case 只會在執行期走到default;
TS 用never讓編譯器幫你檢查——加一個新的變體,所有沒處理它的地方全部編譯失敗。實務價值:新增一種訂單狀態、一種事件類型、一種錯誤種類時,
編譯器會列出所有需要更新的地方,而不是等到線上才發現漏了一處。更簡潔的寫法(把它抽成工具函式):
export function assertNever(x: never): never { throw new Error(`unexpected: ${JSON.stringify(x)}`); } // default: return assertNever(s);5. ★ 讓「不合法的狀態」根本無法被表示
這是判別聯集最有價值的用法,而且它解決的是設計問題不是語法問題:
// ❌ 用可選欄位:四種欄位組合,但只有三種是合法的 type Request = { status: "loading" | "success" | "error"; data?: Data; // ★ status 是 success 時才該有 error?: Error; // ★ status 是 error 時才該有 }; // → 每次用 data 都要 `if (req.data)`,而且 {status:"success"} 沒有 data 也編得過 // ✅ 判別聯集:不合法的組合根本不存在 type Request = | { status: "loading" } | { status: "success"; data: Data } // ★ 保證有 data | { status: "error"; error: Error }; // ★ 保證有 error if (req.status === "success") { render(req.data); // ★ 不用檢查,型別已經保證了 }★ “Make illegal states unrepresentable.”
每個可選欄位(?)都是一次「我沒想清楚」的自白——
它讓合法狀態的數量變成 2n,而其中大部分是無意義的。6. ★ 實務上最常用的三個場景
① API 回應
type Result<T, E = Error> = | { ok: true; value: T } | { ok: false; error: E }; const r = await fetchUser(id); if (!r.ok) return handle(r.error); use(r.value); // ★ 這裡 value 保證存在★ 這是 Go 的
(T, error)在 TS 裡的對應,而且比try/catch更能讓型別系統參與。② 狀態機(見 狀態模式)
③ 事件 / Action(Redux 的 action 就是判別聯集,見 命令模式)7. 判別欄位的選擇
做法 評價 kind/type/status的字串字面量★ 最常用,可序列化、好讀、好 debug success: true / false布林只有兩種時可以,多於兩種就不行 用 in運算子判斷欄位存在⚠️ 可行但脆弱,重構時容易壞 class + instanceof不可序列化(JSON 來回會失去原型),跨網路邊界不能用 ★ 要跨網路或存進資料庫 → 一定用字串字面量的判別欄位,因為它 JSON 化之後還在。
🧪 我實際套用的紀錄
- 2026-08-28:(待填)
⚠️ 注意 / 什麼時候不適用
- ★ 判別欄位必須是字面量型別。宣告成
kind: string就失去縮窄能力——
用as const或直接在 type 裡寫字面量(見 工具-as-const與推論寬窄)。- ★
default裡的never檢查在strict關閉時無效。要開strictNullChecks(見 工具-tsconfig基準設定)。- 從外部來的資料不會自動符合型別。
JSON.parse回傳any——
★ 邊界上一定要驗證(zod / valibot / 手寫 type guard),否則型別只是自我安慰。- 變體太多會很囉嗦。超過七八個變體時,考慮用「共同欄位 + 一個小的判別聯集」而不是全部攤平。
- 不要為了「以後可能有第二種」而先做成聯集。一種就是一種。
- Go 沒有這個。Go 的對應做法是 struct + 一個
Kind欄位 + 註解說明「哪些欄位在哪些 Kind 下有效」——
沒有編譯期保證。跨語言的 API 設計要注意這個落差(見 訪問者模式)。- 窮盡性檢查只對「聯集」有效,對
enum或string沒用。🔗 相關工具
- 工具-as-const與推論寬窄 —— 判別欄位為什麼要是字面量
- 工具-satisfies與型別標註 —— 保留字面量型別又檢查形狀
- 工具-tsconfig基準設定 —— strict 是前提
- 工具-型別推導還是解耦 —— 型別該推導還是該明寫
- 訪問者模式 —— ★ TS 用判別聯集完勝訪問者模式
- 狀態模式 —— 讓不合法的狀態轉換無法被表示
- 命令模式 —— Redux action 就是判別聯集
- Total TypeScript Essentials
🎯 推論與標註
🎯 什麼情境該想到我
當你「幫變數標了型別,結果存取它的 key 反而壞掉」的時候。
⚙️ 怎麼用(步驟 / 公式)
三條規則,記這個就夠
寫法 誰贏 後果 const x: T = {...}變數贏 值通過檢查,然後值的型別被丟掉,只剩 Tconst x = {...}值贏 保留精確推論,但沒有任何約束 const x = {...} satisfies T兩者兼得 用 T檢查,但保留值的推論結果為什麼標註會壞事
const config: Record<string, Color> = { foreground: { r: 255, g: 255, b: 255 }, border: "transparent", }; config.foreground.r; // ❌ 錯:TS 只知道值是 Color,不知道 foreground 存在標註之後 TS 只記得「這是一個
Record<string, Color>」——它不知道有哪些 key,也不知道每個 key 是Color的哪一支。換成 satisfies
const config = { foreground: { r: 255, g: 255, b: 255 }, border: "transparent", } satisfies Record<string, Color>; config.foreground.r; // ✅ config.border.toUpperCase(); // ✅ TS 知道這支是 string config.primary = 123; // ✅ 仍然會被擋下來
satisfies也會收窄常被誤解成「不影響值的型別」,其實會:
const album = { format: "Vinyl" } satisfies Album; // album.format 推論為 "Vinyl",不是 "CD" | "Vinyl" | "Digital"所以要把它傳給只吃
"Vinyl"的函式時,satisfies可行、標註不行。4. ★ 三種寫法的完整對照
type Config = Record<string, { url: string; retries: number }>; // ① 標註(annotation)—— 檢查了,但★ 具體資訊被吃掉 const a: Config = { prod: { url: "...", retries: 3 } }; a.prod; // ★ 可以,但 a.typo 也「可以」(因為型別是 Record<string, ...>) // ② as(斷言)—— ★ 不檢查,只是叫編譯器閉嘴 const b = { prod: { url: "...", retries: "3" } } as Config; // ★ retries 是字串也不報錯! // ③ satisfies —— ★ 檢查形狀,同時保留具體推論 const c = { prod: { url: "...", retries: 3 } } satisfies Config; c.prod; // ✅ c.typo; // ★ 編譯錯誤:Property 'typo' does not exist
檢查形狀 保留具體型別 錯誤時會報 : Type標註✅ ❌ 被拓寬成標註的型別 ✅ as Type斷言❌ ⚠️ 變成斷言的型別 ❌ 危險 ★ satisfies Type✅ ✅ ✅ ★
as不是型別轉換,是「我保證,別檢查」。
它是 TS 裡最容易製造執行期錯誤的語法——看到as就該問「為什麼需要它」。
(少數正當用途:as const、縮窄unknown、跟型別定義不完整的第三方庫打交道。)5. ★ 最有價值的組合:
satisfies+keyof typeofconst routes = { home: { path: "/", auth: false }, profile: { path: "/me", auth: true }, admin: { path: "/admin", auth: true }, } satisfies Record<string, { path: string; auth: boolean }>; type RouteName = keyof typeof routes; // ★ "home" | "profile" | "admin" function go(name: RouteName) { ... } go("home"); // ✅ go("hom"); // ★ 編譯錯誤★ 這個組合解決了「兩份清單要同步維護」的問題:
型別RouteName從實作自動推導出來——加一個路由,型別自動跟著更新。
不用維護一個type RouteName = "home" | "profile" | "admin"的重複定義。Go 沒有這個能力:Go 得手動維護
const列舉與 map 兩份,加一筆忘了另一邊也不會報錯。6. 三個高頻實務場景
① 設定物件的預設值
const defaults = { timeout: 30_000, retries: 3 } satisfies Partial<Options>; const opts = { ...defaults, ...userOpts }; // ★ 見 [[建造者模式]]② 策略表
const handlers = { create: (p: CreatePayload) => { ... }, update: (p: UpdatePayload) => { ... }, } satisfies Record<string, (p: any) => void>; type Action = keyof typeof handlers; // ★ 自動推導→ 見 策略模式
③ 顏色/常數表
const theme = { primary: "#0af", danger: "#f33", } satisfies Record<string, `#${string}`>; // ★ 檢查是合法的 hex 形狀 theme.primary; // ★ 型別是 "#0af"(字面量),不是 string7. ★ 什麼時候該用「標註」而不是
satisfies
用標註 : T用 satisfies T函式參數與回傳值(★ 這是 API 契約,應該明寫) 常數/設定物件 希望隱藏具體實作細節 希望保留具體型別供後續推導 變數之後會被重新賦值成別的值 值是固定的 ★ 判準:這個型別是「契約」還是「檢查」?
契約(別人依賴它)→ 標註;只是想確認我寫對了 →satisfies。
(更完整的討論見 工具-型別推導還是解耦。)🧪 我實際套用的紀錄
- 2026-08-28:(待填)
⚠️ 注意 / 什麼時候不適用
- ★
satisfies需要 TypeScript 4.9+。舊專案要先升版。- ★ 不要用
satisfies取代所有標註。函式簽章應該明寫型別——
那是 API 契約,讓它被推導會讓改動意外地變成 breaking change(見 工具-型別推導還是解耦)。satisfies不會讓值變成不可變。要不可變要配as const(見 工具-as-const與推論寬窄):const x = { a: 1 } as const satisfies Config; // ★ 兩個一起用as的正當用途很少。看到它就該問:是型別定義不夠好,還是我在騙編譯器?
as unknown as T幾乎一定是設計問題。- ★ 執行期沒有型別。
satisfies只在編譯期檢查;外部資料(API 回應、JSON)仍然要執行期驗證
(zod / valibot),否則型別只是裝飾。- 推導出來的型別會出現在錯誤訊息與 hover 裡。巨大的物件字面量會讓型別提示變得難讀——
這時明確標註反而更好維護。🔗 相關工具
- 工具-as-const與推論寬窄 —— 另一半:怎麼控制推論的寬窄
- 工具-型別推導還是解耦 —— ★ 什麼時候該推導、什麼時候該明寫
- 工具-判別聯集 —— 判別欄位需要字面量型別
- 工具-tsconfig基準設定 —— 前置設定
- 策略模式 ——
satisfies+keyof typeof的策略表- 建造者模式 —— TS 用物件字面量取代 Builder
- Total TypeScript Essentials
🎯 什麼情境該想到我
當你「明明傳的就是那個字串,TS 卻說
string不能指派給"rock" | "country"」的時候。⚙️ 怎麼用(步驟 / 公式)
先理解為什麼會被推寬——TS 是照「還能不能被改」推的
宣告 推論結果 原因 let x = "rock"stringlet可以重新指派,所以推寬以容納未來的值const x = "rock""rock"const不能重新指派,可以安全地推成字面量const o = { s: "rock" }{ s: string }⚠️ 物件屬性即使在 const下仍可變,所以照樣推寬第三列是最常踩的坑:你用了
const,但物件裡面的屬性還是被推成string。三種修法,按情境挑
// 1. 標註變數:可以重新指派,但限制在聯集內 let genre: AlbumGenre = "rock"; // 2. 標註物件:整體受約束(但會抹掉精確推論) const attrs: AlbumAttributes = { status: "on-sale" }; // 3. as const:整個結構凍成唯讀的字面量 const attrs = { status: "on-sale" } as const;
as const拿來做 JS 風格的列舉這是它最有價值的用法之一——一份來源,同時給你執行期的值與型別:
const albumTypes = { CD: "cd", VINYL: "vinyl", DIGITAL: "digital", } as const; type AlbumType = (typeof albumTypes)[keyof typeof albumTypes]; // "cd" | "vinyl" | "digital"比起手寫一份
type AlbumType = "cd" | "vinyl" | "digital",這樣不會有兩份會走鐘的來源。4. ★ 推論寬窄的完整規則表
let a = "hello"; // string ★ let → 拓寬 const b = "hello"; // "hello" ★ const → 字面量 const o = { k: "v" }; // { k: string } ★ 物件屬性「仍然」被拓寬 const o2 = { k: "v" } as const; // { readonly k: "v" } const arr = [1, 2]; // number[] ★ 陣列被拓寬 const t = [1, 2] as const; // readonly [1, 2] ★ 變成元組(tuple)★ 最容易踩的一條:
const只讓「變數本身」不可重新賦值,不讓「內部屬性」保持字面量。
所以const o = { kind: "circle" }的o.kind型別是string,
拿去當判別聯集的判別欄位就失效了(見 工具-判別聯集)。5. ★ 三個
as const真正必要的場景① 判別聯集的判別欄位
const action = { type: "increment", by: 1 }; dispatch(action); // ★ 錯誤:type 是 string,不符合 Action 聯集 const action = { type: "increment", by: 1 } as const; dispatch(action); // ✅② 從常數表推導型別(★ 最有價值的用法)
const ROLES = ["admin", "editor", "viewer"] as const; type Role = typeof ROLES[number]; // ★ "admin" | "editor" | "viewer" function can(r: Role) { ... } can("admin"); // ✅ can("admn"); // ★ 編譯錯誤 // 執行期也能用同一份資料 if (ROLES.includes(input as Role)) { ... }★ 「一份資料,同時給執行期用和給型別用」 —— 這是 TS 最實用的型別技巧之一。
不用維護type Role = ...和const ROLES = [...]兩份會不同步的清單。③ 元組(tuple)
function useToggle(): [boolean, () => void] { ... } // 內部 return [v, fn] as const ← ★ 否則會被推成 (boolean | (() => void))[]6. ★
as constvssatisfies:該用哪個
你要的 用 保留字面量 + 變成 readonly as const檢查形狀符合某個型別 + 保留字面量 satisfies T★ 兩者都要 as const satisfies T(順序不能反)const theme = { primary: "#0af", danger: "#f33", } as const satisfies Record<string, `#${string}`>; // ★ 既檢查了是合法 hex,又保留了 "#0af" 這個字面量,而且是 readonly7.
readonly的邊界(常見誤解)const c = { a: 1 } as const; c.a = 2; // ★ 編譯錯誤 // ★ 但執行期完全沒有保護 const d = c as { a: number }; d.a = 2; // ★ 編譯通過,而且真的改掉了★
readonly是純編譯期的。執行期要真正凍結要用Object.freeze()——
而Object.freeze只凍一層(巢狀物件仍可改)。兩者是互補的,不是替代:
as const給開發期的保護,Object.freeze給執行期的。8. ★ 與 Go 的對照
Go 沒有字面量型別,
const ROLES = []string{...}無法產生型別。
Go 的作法是:type Role string const ( RoleAdmin Role = "admin" RoleEditor Role = "editor" )★ 但 Go 的這個做法不防止
Role("typo")—— 它只是個字串別名。
TS 的typeof ROLES[number]在這一點上是嚴格更強的。
(跨語言 API 設計時要知道這個落差,見 工具-判別聯集。)🧪 我實際套用的紀錄
- 2026-08-28:(待填)
⚠️ 注意 / 什麼時候不適用
- ★
as const會讓整個結構變readonly(遞迴)。
之後要push、sort、splice就過不了——需要可變副本時用[...arr]展開。- ★ 不要對「會變的資料」用
as const。它是給常數、設定、列舉用的,不是給執行期資料用的。as const不是as。前者是推論指示(安全),後者是型別斷言(危險)。名字像但完全不同。- 巨大的
as const物件會讓型別提示與錯誤訊息很難讀,編譯也會變慢。- ★ 執行期沒有保護(見上)。安全性相關的不可變要靠
Object.freeze或結構設計。as const satisfies T的順序不能反(satisfies T as const不合法)。- 從 API 回來的資料不會有字面量型別。
JSON.parse是any——
邊界上要驗證 + narrow,as const幫不上忙。🔗 相關工具
- 工具-satisfies與型別標註 —— ★ 兩者怎麼搭配
- 工具-判別聯集 —— 判別欄位為什麼需要字面量
- 工具-型別推導還是解耦 —— 推導的邊界
- 工具-tsconfig基準設定 —— strict 是前提
- 值物件 —— 不可變性的設計思路
- Total TypeScript Essentials
🗂 型別怎麼組織
🎯 什麼情境該想到我
當你猶豫「這個型別要從既有型別挖出來(
Pick/keyof/typeof),還是老實再寫一份?」的時候。⚙️ 怎麼用(步驟 / 公式)
先認識工具
語法 做什麼 keyof T把 T的 key 抽成聯集typeof v從值取出它的型別 T[K]索引存取:取出某個屬性的型別 Pick<T, K>/Omit<T, K>挑出/排除某些屬性 interface A extends B繼承 ⭐ 判準:兩邊是不是同一個關注點?
該推導 —— 共用同一個關注點
const albumTypes = { CD: "cd", VINYL: "vinyl" } as const; type AlbumType = (typeof albumTypes)[keyof typeof albumTypes];值和型別本來就是同一件事的兩面。解耦的話你要維護兩份會走鐘的來源。
該解耦 —— 關注點不同
// ❌ 誘人但耦合:UI 元件綁死資料層的型別 type AvatarImageProps = Pick<User, "imageUrl" | "name">; // ✅ 各自寫清楚 type AvatarImageProps = { imageUrl: string; name: string };
User是資料型別、AvatarImage是 UI 元件——責任不同、改變的理由也不同。用Pick之後,這個元件不只依賴User的形狀,還依賴它存在於db.ts這件事。★ 核心判準:這個型別是「契約」還是「衍生」
契約(contract) 衍生(derived) 誰依賴它 ★ 外部(別的模組、別的團隊、別的服務) 只有自己 改動的意義 ★ breaking change,要有意識地做 跟著來源走就好 該怎麼寫 ★ 明確標註 ★ 從來源推導 例子 公開 API 的參數與回傳、跨服務的 DTO 內部的中間型別、元件的 props ★ 一句話:「改了實作,這個型別跟著變,是我要的嗎?」
是 → 推導;不是(那會默默破壞下游)→ 明寫。★ 三個「應該推導」的場景
① 從實作推導列舉(見 工具-satisfies與型別標註)
const routes = { home: "/", admin: "/admin" } satisfies Record<string, string>; type RouteName = keyof typeof routes; // ★ 加路由自動更新,不用維護兩份② 從 schema 推導型別(★ 實務上最有價值的一個)
import { z } from "zod"; const UserSchema = z.object({ id: z.string().uuid(), email: z.string().email(), age: z.number().int().min(0), }); type User = z.infer<typeof UserSchema>; // ★ 型別從驗證器推導 // ★ 一份定義,同時給「執行期驗證」和「編譯期型別」 const user = UserSchema.parse(await res.json());★ 這解決了 TS 最根本的弱點:編譯期型別在執行期不存在。
手寫type User+ 手寫驗證 = 兩份會不同步的定義;從 schema 推導 = 一份定義,永遠同步。
同理:sqlc(從 SQL 產型別)、OpenAPI codegen、Prisma、GraphQL codegen。③ 從函式推導
type Params = Parameters<typeof myFunc>; type Result = Awaited<ReturnType<typeof fetchUser>>;★ 三個「應該明寫」的場景
① 公開函式的簽章
// ❌ 讓回傳型別被推導 → 改實作可能意外改變公開 API export function getUser(id: string) { return { id, name: "x" }; // ★ 多加一個內部欄位就變成 API 的一部分 } // ✅ 明寫契約 export function getUser(id: string): User { ... }② 需要隱藏實作細節時 —— 標註成介面,讓呼叫端不能依賴你多回傳的東西。
③ ★ 讓型別錯誤出現在「對的地方」
// ❌ 不標註:錯誤出現在「使用的地方」,離源頭很遠 const config = { retries: "3" }; doWork(config); // ★ 這裡才報錯 // ✅ 標註:錯誤出現在「定義的地方」 const config: Config = { retries: "3" }; // ★ 這裡就報錯★ 標註的位置決定了錯誤訊息的位置——這是很實用的一條。
衍生型別的工具(不要複製貼上)
type PublicUser = Omit<User, "password">; type UserUpdate = Partial<Omit<User, "id">>; type UserCreate = Pick<User, "email" | "password">; type UserId = User["id"];★ 但衍生也會過頭。
Partial<Omit<Pick<User,"a"|"b">,"c">>沒有人看得懂,
hover 出來的提示也是一大坨。超過兩層就給它一個名字。★ 和 Go 的對照
Go 沒有型別層的運算——沒有
Omit、Partial、keyof。對應做法是定義多個 struct(手動維護、會不同步),
或用go:generate產生(sqlc、oapi-codegen)。
★ 和 TS 的z.infer是同一個問題的兩種解法。🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
- ★ 公開 API 一定要明寫。被推導的回傳型別會讓「內部重構」意外變成 breaking change。
- ★ 過度衍生會讓型別無法閱讀。三層以上的
Partial<Omit<...>>要拆開命名。- ★ 型別關聯太緊也是一種耦合。
type B = A讓 A 改動直接波及 B——
如果 B 在語義上是獨立的概念(只是「現在剛好一樣」),就該分開寫。
判準:「A 改了,B 應該跟著改嗎?」 應該 → 關聯;不一定 → 分開。z.infer的代價是執行期的驗證成本。熱路徑上的大物件驗證要量。- 推導出來的型別出現在錯誤訊息裡會很長,會讓錯誤難讀、編譯變慢。
- 不要為了「少打字」而推導。可讀性與契約的穩定性遠比打字量重要。
- Go 沒有這些,跨語言的共用型別要靠 codegen 或 schema(Protobuf、OpenAPI)。
🔗 相關工具
- 工具-satisfies與型別標註 —— 檢查形狀又保留推導
- 工具-as-const與推論寬窄 —— 控制推論寬窄
- 工具-判別聯集 —— 衍生型別常用在聯集上
- 工具-tsconfig基準設定 —— strict 是前提
- 工具-資料編碼與演進 —— 跨服務的型別要靠 schema 而非語言型別
- 工具-讓LLM產出可驗證的產物 —— 同樣是「一份 schema 兩邊用」的思路
- Total TypeScript Essentials
⚙️ 專案設定
🎯 什麼情境該想到我
當你「開一個新的 TS 專案,對著空白的
tsconfig.json發呆」的時候。⚙️ 怎麼用(步驟 / 公式)
第一步:先貼這份基準(適用多數應用)
{ "compilerOptions": { /* Base Options */ "skipLibCheck": true, // 跳過 .d.ts 的型別檢查,加快編譯 "target": "es2022", // 隨時間往上調 "esModuleInterop": true, // CJS 與 ESM 的互通性 "allowJs": true, // 允許引入 .js "resolveJsonModule": true, // 允許引入 .json "moduleDetection": "force", // 所有 .ts 都當成 module,不是 script "isolatedModules": true, // 每個檔案都能被獨立轉譯 /* Strictness */ "strict": true, "noUncheckedIndexedAccess": true // ⭐ 見下方 } }第二步:問自己四個問題,決定要補什麼
問題 要加的設定 用 tsc 轉譯嗎? "module": "NodeNext",並設outDir、sourceMap、verbatimModuleSyntax不是用 tsc(esbuild/Babel 等)? "module": "Preserve"+"noEmit": true要做函式庫嗎? "declaration": true;monorepo 再加"composite": true與"declarationMap": true程式跑在 DOM 嗎? 是 → "lib": ["dom", "dom.iterable", "es2022"];否 →["es2022"]⭐
noUncheckedIndexedAccess是最值得單獨提的一項它讓索引存取回傳
T | undefined,逼你處理「取不到」的情況。strict: true不含這一項,但沒有它,arr[999]會被當成一定有值——這正是最典型的執行期爆炸。順帶:
import type的意義不只是好看純型別的 import 在編譯後會被移除(elide)。問題是模組可能有副作用(例如頂層的
console.log):import type { Album } from "./album"; // 整行都會被移除 import { type Album, createAlbum } from "./album"; // 只移除型別部分
verbatimModuleSyntax就是要求你把這件事講清楚,避免「引入被悄悄拿掉、副作用沒跑」這種難查的問題。🧪 我實際套用的紀錄
- 2026-08-28:(待填)
⚠️ 注意 / 什麼時候不適用
target會過期。書自己就說「等你讀到這本書時,可能該指定更新的版本了」——這份設定要定期回顧。skipLibCheck: true是速度與嚴謹的取捨:它跳過依賴的.d.ts檢查,所以第三方型別的錯不會被你發現。多數專案划算,但要知道自己放棄了什麼。noUncheckedIndexedAccess導入既有專案會炸出一堆錯。新專案直接開;舊專案要排時間。- 框架的 starter 通常已經設好了(Next.js、Vite…),不要無腦覆蓋掉——先看它為什麼那樣設。
🔗 相關工具
- 工具-建構的先決條件 —— 專案開場該先確立的事,設定檔是其中最容易被跳過的一項
- 工具-防禦式編程 ——
noUncheckedIndexedAccess就是把防禦式思維交給編譯器執行
🧰 這本書給我的工具(連到 tools/ 工具卡)
- 工具-判別聯集 — 當我的狀態物件長出一堆「可有可無」的欄位的時候
- 工具-satisfies與型別標註 — 當我「標了型別,存取欄位反而壞掉」的時候
- 工具-as-const與推論寬窄 — 當我「傳個字串進去卻說不能指派給 union」的時候
- 工具-型別推導還是解耦 — 當我猶豫「這個型別要不要從別的型別挖出來」的時候
- 工具-tsconfig基準設定 — 當我開新專案、不知道 tsconfig 該怎麼設的時候
✨ 關鍵重點(Layer 1–2|高亮遞進)
- 貫穿全書的主軸是「寬 ↔ 窄」:
unknown是最寬的型別,never是最窄的。多數 TS 的痛點都是「推論出來的型別比我要的寬」或「我約束得比實際需要更寬」。 unknown與any的差別是安全性:unknown是型別系統的一部分(寬到不能指派給任何東西);any是退出型別檢查,它同時比所有型別更寬也更窄。書的說法很好記——unknown是「我不知道這是什麼」,any是「我不在乎這是什麼」。- 宣告方式決定推論寬窄:
let推成寬型別(因為可以被重新指派),const推成字面量。但物件屬性即使在const底下也會推成寬型別,因為物件本身可變。 - 標註 vs 值,是一場拉鋸:標註變數 → 變數贏,值的型別被丟掉;不標註 → 值贏,但失去約束;
satisfies→ 兩者兼得。 - 推導(deriving)是一種耦合。書明確警告:
Pick之所以誘人,是因為它讓你覺得自己很聰明——但關注點不同的兩個型別,寧可各自寫清楚。 - 章節涵蓋:設定與 IDE(1–3)、基礎型別(4)、聯集與收窄(5)、物件(6)、可變性(7)、類別(8)、TS 專屬特性(9)、型別推導(10)、標註與斷言(11)、詭異的部分(12)、模組與宣告檔(13)、tsconfig(14)、設計你的型別(15)、工具資料夾(16)。
💬 金句原文(Layer 0|讀時貼進來)
- 關於
unknown與any:“
unknownmeans ‘I don’t know what this is’, whileanymeans ‘I don’t care what this is’.” - 關於過度設計型別的心理陷阱(本書最該記的一句):
“What can make decoupling a difficult decision is that deriving can make you feel ‘clever’.
Picktempts us because it uses a more advanced feature of TypeScript, which makes us feel good for applying the knowledge we’ve gained. But often, it’s smarter to do the simple thing, and keep your types decoupled.” - 關於「選配欄位袋」這種壞型別:
“I’d describe this type as a ‘bag of optionals’. It’s a type that’s too loose.”
- 標註與值的三條規則:
“When you use a variable annotation, the variable’s type wins. When you don’t use a variable annotation, the value’s type wins. When you use
satisfies, you can tell TypeScript that a value must satisfy certain criteria, but still allow TypeScript to infer the type.”
🔗 相關
- Skills For Real Engineers — 同一位作者的 agent skill 集合
- 軟體工程 — 主題索引