【TypeScript 入門教學】#04 Union、Literal 與型別收窄

測驗:Union、Literal 與型別收窄

共 5 題,點選答案後會立即顯示結果

1. 在 TypeScript 中,string | number| 代表什麼意思?

  • A. 這個變數同時是 string 和 number
  • B. 這個變數可以是 string 或 number
  • C. 這個變數會先是 string,之後變成 number
  • D. 這是位元運算,會把 string 和 number 做 OR 運算

2. 以下程式碼中,type Status = "loading" | "success" | "error" 屬於什麼型別?

  • A. Union 型別,允許任何字串值
  • B. typeof 收窄,用來檢查型別
  • C. Literal 型別,只能是這三個特定字串之一
  • D. 可辨識聯合,用共同屬性分辨型別

3. 為什麼以下程式碼會報錯?

function printLength(value: string | number) { console.log(value.length); }
  • A. string 沒有 .length 屬性
  • B. 不能在函式參數使用 Union 型別
  • C. console.log 不支援接收 .length 的結果
  • D. number 沒有 .length 屬性,TypeScript 不確定 value 是哪種型別所以不允許

4. 看以下程式碼,TypeScript 如何判斷 animal 是 Fish 還是 Bird?

type Fish = { swim: () => void }; type Bird = { fly: () => void }; function move(animal: Fish | Bird) { if (“swim” in animal) { animal.swim(); } else { animal.fly(); } }
  • A. 用 typeof 檢查 animal 的基本型別
  • B. 用 in 檢查物件有沒有 “swim” 屬性,有的話就知道是 Fish
  • C. 用 instanceof 檢查 animal 是否為 Fish 的實例
  • D. 用可辨識聯合,透過共同的 type 屬性來分辨

5. 以下程式碼中,user?.name ?? "匿名" 的完整意思是什麼?

const name = user?.name ?? “匿名”;
  • A. 如果 user.name 是空字串,就用 “匿名”
  • B. 保證 user 不是 null,然後取得 name
  • C. 如果 user 是 null/undefined 就回傳 undefined,再用 ?? 在 null/undefined 時改用 “匿名”
  • D. 先斷言 user 存在,如果 name 是 falsy 值就用 “匿名”

一句話說明

讓一個變數可以是好幾種型別,但 TypeScript 會逼你「確認清楚了」才能用。

為什麼要學這個?

你請 AI 幫你寫 TypeScript 時,產出的程式碼裡很常出現 string | numberif (typeof x === "string") 這類寫法。這不是 AI 在搞花樣 — 這是 TypeScript 的核心機制。看不懂這些,你就很難判斷 AI 寫的程式碼到底對不對。


1. Union 型別:「可以是 A 或 B」

最小範例

let value: string | number;
value = "hello";   // OK
value = 42;        // OK
value = true;      // 錯誤!只能是 string 或 number
Code language: JavaScript (javascript)

逐行翻譯

let value: string | number;
//         ↑ 這個 | 念作「或」
//         意思是:value 可以是文字,也可以是數字
Code language: JavaScript (javascript)

一句話| 就是「或」的意思。string | number 代表「可以是文字或數字」。

常見變化

AI 最常產出的幾種 Union 寫法:

變化 1:允許 null

let user: User | null;
Code language: JavaScript (javascript)

翻譯:user 可以是一個 User 物件,也可以是 null(還沒有值)。

變化 2:函式的參數

function printId(id: string | number) {
    console.log(id);
}
Code language: JavaScript (javascript)

翻譯:printId 這個函式接受文字或數字當 id。

變化 3:函式的回傳值

function find(id: number): User | undefined {
    // ...
}
Code language: JavaScript (javascript)

翻譯:find 可能回傳一個 User,也可能回傳 undefined(找不到)。


2. Literal 型別:「只能是這幾個特定值」

最小範例

type Direction = "up" | "down" | "left" | "right";

let dir: Direction;
dir = "up";       // OK
dir = "diagonal"; // 錯誤!只能是那四個值之一
Code language: JavaScript (javascript)

逐行翻譯

type Direction = "up" | "down" | "left" | "right";
//   ↑ 定義一個型別叫 Direction
//              ↑ 它只能是這四個字串之一

let dir: Direction;
//       ↑ dir 只接受 "up"、"down"、"left"、"right"
Code language: JavaScript (javascript)

一句話:Literal 型別就是「把允許的值一個一個列出來」。不是那幾個值,TypeScript 就報錯。

常見變化

變化 1:狀態值

type Status = "loading" | "success" | "error";
Code language: JavaScript (javascript)

翻譯:狀態只能是這三種之一,不會出現打錯字的情況。

變化 2:數字 Literal

type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;

翻譯:骰子只能是 1 到 6。

變化 3:搭配函式參數

function move(direction: "up" | "down" | "left" | "right") {
    // ...
}
move("up");      // OK
move("forward"); // 錯誤!
Code language: JavaScript (javascript)

翻譯:move 只接受那四個方向的字串,別的一律不行。


3. 型別收窄(Narrowing):「先檢查,再使用」

這是整篇最重要的概念。

問題:Union 型別不能直接用

function printLength(value: string | number) {
    console.log(value.length);
    //                ↑ 錯誤!number 沒有 .length
}
Code language: JavaScript (javascript)

TypeScript 不知道 value 是 string 還是 number,所以不讓你直接用 .length(因為數字沒有 .length)。

解法:先用 typeof 檢查

function printLength(value: string | number) {
    if (typeof value === "string") {
        console.log(value.length);   // OK!TypeScript 知道這裡是 string
    } else {
        console.log(value);          // 這裡 TypeScript 知道是 number
    }
}
Code language: JavaScript (javascript)

一句話:TypeScript 會「跟著你的 if 判斷走」。你檢查了是 string,它就讓你當 string 用。這就是「收窄」– 把範圍縮小。


4. typeof 收窄:最常見的檢查方式

最小範例

function process(input: string | number | boolean) {
    if (typeof input === "string") {
        // 這裡 input 是 string
        console.log(input.toUpperCase());
    } else if (typeof input === "number") {
        // 這裡 input 是 number
        console.log(input.toFixed(2));
    } else {
        // 這裡 input 是 boolean
        console.log(input ? "yes" : "no");
    }
}
Code language: JavaScript (javascript)

翻譯

typeof input === "string"   // input 是不是文字?
typeof input === "number"   // input 是不是數字?
typeof input === "boolean"  // input 是不是布林值?
Code language: JavaScript (javascript)

一句話typeof 問的是「這個值是什麼基本型別」。TypeScript 看到你用了 typeof 檢查,就自動知道在那個 if 區塊裡,變數是什麼型別。


5. in 收窄:「這個物件有沒有某個屬性」

最小範例

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
    if ("swim" in animal) {
        animal.swim();    // TypeScript 知道這是 Fish
    } else {
        animal.fly();     // TypeScript 知道這是 Bird
    }
}
Code language: JavaScript (javascript)

逐行翻譯

"swim" in animal
// ↑ 問:animal 這個物件裡面有沒有 "swim" 這個屬性?
// 如果有,TypeScript 就推斷它是 Fish
Code language: JavaScript (javascript)

一句話"屬性名" in 物件 就是在問「這個物件有沒有這個屬性」。有的話,TypeScript 就知道它是哪個型別。


6. instanceof 收窄:「這個是不是某個 class 的實例」

最小範例

function logDate(value: string | Date) {
    if (value instanceof Date) {
        console.log(value.getFullYear());  // TypeScript 知道是 Date
    } else {
        console.log(value.toUpperCase());  // TypeScript 知道是 string
    }
}
Code language: JavaScript (javascript)

翻譯

value instanceof Date
// ↑ 問:value 是不是一個 Date 物件?
Code language: JavaScript (javascript)

一句話instanceof 用來檢查「是不是某個 class 製造出來的」。typeof 管基本型別(string、number),instanceof 管 class 實例(Date、Error、自定義 class)。


7. 可辨識聯合(Discriminated Unions):靠共同屬性分辨

這是 AI 產出的程式碼裡非常常見的模式。

最小範例

type Success = { status: "success"; data: string };
type Failure = { status: "error"; message: string };
type Result = Success | Failure;

function handle(result: Result) {
    if (result.status === "success") {
        console.log(result.data);      // OK!TypeScript 知道這是 Success
    } else {
        console.log(result.message);   // OK!TypeScript 知道這是 Failure
    }
}
Code language: JavaScript (javascript)

逐行翻譯

type Success = { status: "success"; data: string };
//               ↑ status 固定是 "success" 這個字串(Literal 型別)

type Failure = { status: "error"; message: string };
//               ↑ status 固定是 "error"

type Result = Success | Failure;
//            ↑ Result 是 Success 或 Failure

if (result.status === "success") {
//  ↑ 用共同的 status 屬性來分辨是哪一種
//    TypeScript 看到 status 是 "success",就知道整個物件是 Success 型別
}
Code language: JavaScript (javascript)

一句話:幾個型別共用一個屬性(如 status),但值不同。TypeScript 根據那個屬性的值,就能判斷是哪個型別。

AI 常用的場景

API 回應

type ApiResponse =
    | { ok: true; data: User[] }
    | { ok: false; error: string };
Code language: JavaScript (javascript)

翻譯:API 回來的東西,不是成功帶資料,就是失敗帶錯誤訊息。用 ok 欄位來分辨。

Redux Action(狀態管理)

type Action =
    | { type: "ADD_TODO"; text: string }
    | { type: "TOGGLE_TODO"; id: number }
    | { type: "DELETE_TODO"; id: number };
Code language: JavaScript (javascript)

翻譯:三種操作,用 type 欄位來分辨是哪一種。


8. null 和 undefined 的處理

TypeScript 預設不允許把 null 或 undefined 塞給其他型別。你必須明確宣告並處理。

最小範例

function greet(name: string | null) {
    if (name !== null) {
        console.log(`Hello, ${name}`);  // TypeScript 知道這裡不是 null
    } else {
        console.log("Hello, stranger");
    }
}
Code language: JavaScript (javascript)

常見寫法速查

// 寫法 1:if 判斷
if (value !== null && value !== undefined) {
    // 安全使用 value
}

// 寫法 2:可選鏈(Optional Chaining)
const name = user?.name;
// ↑ 如果 user 是 null/undefined,name 就是 undefined,不會報錯

// 寫法 3:空值合併(Nullish Coalescing)
const name = user?.name ?? "匿名";
// ↑ 如果 user?.name 是 null 或 undefined,就用 "匿名"

// 寫法 4:非空斷言(謹慎使用!)
const name = user!.name;
// ↑ 告訴 TypeScript「我保證 user 不是 null」
// 如果你猜錯了,執行時會爆掉
Code language: JavaScript (javascript)

翻譯對照

代碼 翻譯
user?.name 如果 user 存在,取 name;否則回傳 undefined
name ?? "預設" 如果 name 是 null 或 undefined,用 “預設”
user!.name 我保證 user 不是 null(危險,少用)

必看懂 vs 知道就好

必看懂(AI 產出的程式碼天天都有)

語法 範例 意思
Union 型別 string \| number 可以是文字或數字
Literal 型別 "success" \| "error" 只能是這幾個值
typeof 收窄 if (typeof x === "string") 確認是什麼基本型別
in 收窄 if ("name" in obj) 確認物件有沒有某屬性
可辨識聯合 if (result.status === "ok") 用共同屬性分辨型別
可選鏈 user?.name 安全存取,不怕 null
空值合併 value ?? "預設" null/undefined 時用預設值

知道就好(遇到再查)

  • 自訂型別守衛(Type Guard)function isFish(pet: Fish | Bird): pet is Fish — 自己寫一個函式來判斷型別。
  • never 型別:在窮舉所有可能後出現的型別,代表「不應該走到這裡」。
  • as 型別斷言const x = value as string — 強制告訴 TypeScript 這是什麼型別,除非你很確定,否則不要亂用。

Vibe Coder 檢查點

看到 AI 產出的程式碼裡有 Union 型別或型別收窄時,確認以下幾點:

  • [ ] Union 型別的每一種情況都有處理嗎?(比如 string | number,有沒有分別處理 string 和 number 的情況?)
  • [ ] 有用到 !(非空斷言)嗎?如果有,AI 真的保證那個值不會是 null 嗎?
  • [ ] 可辨識聯合的每個 case 都有覆蓋到嗎?(比如 API 回應有 success 和 error,兩種都有處理嗎?)
  • [ ] 可選鏈 ?. 後面的邏輯對嗎?(如果值是 undefined,後續有沒有正確處理?)

總結

概念 一句話說明
Union 型別 一個變數可以是好幾種型別,用 \| 隔開
Literal 型別 把允許的值一個一個列出來
型別收窄 用 if 檢查後,TypeScript 自動知道是哪種型別
typeof 檢查基本型別(string、number、boolean)
in 檢查物件有沒有某個屬性
instanceof 檢查是不是某個 class 的實例
可辨識聯合 用共同屬性(如 status、type)的值來分辨型別
?. 和 ?? 安全處理 null/undefined

核心心法:TypeScript 的 Union 型別是「先放寬,再收窄」。宣告時允許多種型別,使用時逼你先確認是哪一種。看到 AI 寫的那些 if 判斷,多半是 TypeScript 逼它寫的 — 不寫就會報錯。

進階測驗:Union、Literal 與型別收窄

測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。

1. 你正在開發一個 API 回應處理函式。AI 幫你產出了以下型別定義,你需要寫一個函式來處理回應。哪種寫法最正確且安全? 情境題

type ApiResponse = | { ok: true; data: User[] } | { ok: false; error: string }; function handleResponse(res: ApiResponse) { // 該怎麼寫? }
  • A. 直接存取 res.data,因為大部分時候 API 會成功
  • B. 用 typeof res.ok === "boolean" 來收窄型別
  • C. 用 if (res.ok) 來判斷,在 true 分支存取 data,在 false 分支存取 error
  • D. 用 res as { ok: true; data: User[] } 強制斷言型別後存取 data

2. 你請 AI 幫你寫一個函式,接受字串或數字 ID,需要回傳格式化後的字串。AI 產出了以下程式碼,你需要判斷哪個版本最符合 TypeScript 的最佳實踐。 情境題

// 版本 A function formatId(id: string | number): string { return “ID-” + id.toString(); } // 版本 B function formatId(id: string | number): string { if (typeof id === “number”) { return “ID-” + id.toFixed(0); } return “ID-” + id.toUpperCase(); } // 版本 C function formatId(id: string | number): string { return “ID-” + (id as string).toUpperCase(); } // 版本 D function formatId(id: string | number): string { return “ID-” + id!.toString(); }
  • A. 版本 A — 簡單直接,toString() 兩種型別都有
  • B. 版本 B — 用 typeof 收窄後,對不同型別做不同處理
  • C. 版本 C — 用 as 斷言比較簡潔
  • D. 版本 D — 用 ! 確保不是 null 就安全了

3. 你在 code review AI 產出的程式碼,看到以下處理使用者資料的片段。根據文章的 Vibe Coder 檢查點,哪個問題最應該優先確認? 情境題

type UserResult = | { status: “active”; profile: Profile } | { status: “banned”; reason: string } | { status: “pending”; verifyUrl: string }; function showUser(result: UserResult) { if (result.status === “active”) { renderProfile(result.profile); } else if (result.status === “banned”) { showBanMessage(result.reason); } }
  • A. 可辨識聯合的 “pending” case 沒有被處理,少了一個分支
  • B. 應該用 typeof 而不是比較 status 來收窄型別
  • C. result 可能是 null,需要先做 null 檢查
  • D. 應該用 in 運算子來檢查 profile 屬性是否存在

4. 以下程式碼在 TypeScript 中出現編譯錯誤。錯誤的根本原因是什麼? 錯誤診斷

function processValue(val: string | number | boolean) { if (typeof val === “string”) { console.log(val.toUpperCase()); } else { console.log(val.toFixed(2)); // Error! } }
  • A. typeof 無法用來收窄 Union 型別
  • B. else 區塊中 val 是 number | boolean,boolean 沒有 toFixed 方法
  • C. toFixed 不是 number 的有效方法
  • D. 應該用 instanceof 而不是 typeof 來檢查 string

5. 以下程式碼中,開發者試圖安全地存取可能為 null 的使用者資料,但有一行寫法有潛在風險。是哪一行? 錯誤診斷

function displayUser(user: User | null) { const name = user?.name ?? “匿名”; // 第 1 行 const email = user!.email; // 第 2 行 const age = user?.age ?? 0; // 第 3 行 console.log(name, email, age); }
  • A. 第 1 行:?? 運算子不能和 ?. 一起使用
  • B. 第 2 行:user! 是非空斷言,如果 user 是 null 執行時會報錯
  • C. 第 3 行:age 是數字不能用 ?? 給預設值
  • D. 所有行都沒問題,TypeScript 會自動處理 null

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *