← Programming Core

TYPESCRIPT CORE · 93

Narrowing、unknown、null:用 runtime evidence 縮小型別

TypeScript 最有價值的能力之一,不是你「宣告得很有自信」,而是它要求你在使用不確定資料前先取得 evidence。

Learning outcomes

1. Narrowing:union 不是問題,沒證據就亂用才是

function format(value: string | number) {
  if (typeof value === "string") {
    return value.toUpperCase();
  }

  return value.toFixed(2);
}

進入 if branch 後,TypeScript 根據 runtime check 知道 value 是 string;else path 剩下 number。

2. unknown API data:先驗最基本 shape

function isRecord(value: unknown):
  value is Record<string, unknown> {
  return typeof value === "object"
    && value !== null;
}

typeof null 歷史上是 "object",所以一定要另外排除 null。

3. 寫一個真正的 Course type guard

function isCourse(value: unknown): value is Course {
  if (!isRecord(value)) return false;

  return typeof value.id === "number"
    && typeof value.title === "string"
    && (
      typeof value.score === "number"
      || value.score === null
    );
}
const raw: unknown = await loadCourse(1);

if (!isCourse(raw)) {
  throw new Error("Invalid course payload");
}

// 這裡 raw 已被 narrow 成 Course
console.log(raw.title);

4. Assertion vs validation

raw as Course

你告訴 checker「相信我」。runtime 沒有新增任何檢查。

isCourse(raw)

真的執行 runtime checks,通過後 checker 才縮小型別。

5. Optional chaining 與 nullish coalescing

const city =
  user.profile?.address?.city
  ?? "Unknown";

?. 是安全地在 null/undefined 時停止 property access;?? 只在左邊是 null/undefined 時使用 fallback。

6. Project checkpoint:Typed Course API Client v3

async function fetchCourse(id: number): Promise<Course> {
  const raw = await loadCourse(id);

  if (!isCourse(raw)) {
    throw new Error("Invalid API payload");
  }

  return raw;
}

async function refreshCourse(id: number) {
  state = { kind: "loading" };

  try {
    const course = await fetchCourse(id);

    state = {
      kind: "success",
      data: course
    };
  } catch (error) {
    state = {
      kind: "error",
      message:
        error instanceof Error
          ? error.message
          : "Unknown error"
    };
  }
}

現在外部 unknown → validation → internal trusted Course 的 boundary 才真正成立。

7. Debug evidence:型別「看起來對」但 runtime 還是炸

如果 code 裡用了大量 as、any,type checker 的綠燈不能當 runtime safety evidence。第一步應檢查 boundary 上真正收到的 payload。

Knowledge check

  1. 為什麼只檢查 typeof value === "object" 不夠?
  2. 把 isCourse() 加上 score 0–100 的 runtime range check。
  3. 解釋 as Course 與 type guard 的差別。
  4. TypeScript Lab 中刻意讓 API payload 不符合宣告,觀察 compile 與 runtime 的邊界。