📘 Total TypeScript Essentials

🧩 型別塑形

🎯 什麼情境該想到我

當你的型別變成一個「選配欄位袋(bag of optionals)」——一堆 ?: 欄位,而且沒人說得清哪些欄位會同時出現的時候。

⚙️ 怎麼用(步驟 / 公式)

病徵:太鬆的型別

type State = {
  status: 'loading' | 'success' | 'error'
  error?: string
  data?: string
}

問題有兩層:

  1. 存取時型別不準——判斷完 status === 'error',state.error 依然是 string | undefined。
  2. 能造出不可能發生的值——{ status: 'loading', error: '...', data: '...' } 完全合法。

解法:把欄位綁到它真正屬於的那個狀態

type State =
  | { status: 'loading' }
  | { status: 'error'; error: string }
  | { status: 'success'; data: string }
  1. 選一個判別欄位(discriminant):一個字面量型別、且每個成員都不同的共同屬性(這裡是 status)。
  2. 把每個狀態拆成獨立的物件型別。
  3. 只把該狀態真正需要的資料放進去。

現在判斷完 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.”
每個可選欄位(?)都是一次「我沒想清楚」的自白——
它讓合法狀態的數量變成 ,而其中大部分是無意義的。

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 沒用。

🔗 相關工具

🎯 推論與標註

🎯 什麼情境該想到我

當你「幫變數標了型別,結果存取它的 key 反而壞掉」的時候。

⚙️ 怎麼用(步驟 / 公式)

三條規則,記這個就夠

寫法誰贏後果
const x: T = {...}變數贏值通過檢查,然後值的型別被丟掉,只剩 T
const 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 typeof

const 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"(字面量),不是 string

7. ★ 什麼時候該用「標註」而不是 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 裡。巨大的物件字面量會讓型別提示變得難讀——
    這時明確標註反而更好維護。

🔗 相關工具

🎯 什麼情境該想到我

當你「明明傳的就是那個字串,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 const vs satisfies:該用哪個

你要的用
保留字面量 + 變成 readonlyas const
檢查形狀符合某個型別 + 保留字面量satisfies T
★ 兩者都要as const satisfies T(順序不能反)
const theme = {
  primary: "#0af",
  danger:  "#f33",
} as const satisfies Record<string, `#${string}`>;
// ★ 既檢查了是合法 hex,又保留了 "#0af" 這個字面量,而且是 readonly

7. 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 幫不上忙。

🔗 相關工具

🗂 型別怎麼組織

🎯 什麼情境該想到我

當你猶豫「這個型別要從既有型別挖出來(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)。

🔗 相關工具

⚙️ 專案設定

🎯 什麼情境該想到我

當你「開一個新的 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…),不要無腦覆蓋掉——先看它為什麼那樣設。

🔗 相關工具