Back to Blog
深入理解 TypeScript Type Guards 與 Type Narrowing
📝 Dev Notes

深入理解 TypeScript Type Guards 與 Type Narrowing

B
Blake
Dec 15, 2025 By Blake 23 min read
為什麼 typeof [] 是 'object'?學會 Type Guards 讓你不再踩雷

前言:一個讓人困惑的面試題

面試官:「typeof [] 的結果是什麼?」

console.log(typeof []);        // 'object'
console.log(typeof null);      // 'object'
console.log(typeof {});        // 'object'
console.log(typeof new Date()); // 'object'

全部都是 'object'!這就是為什麼我們需要 Type Guards


JavaScript typeof 的限制

typeof 只能辨識 7 種型別

typeof 'hello'     // 'string'
typeof 42          // 'number'
typeof true        // 'boolean'
typeof undefined   // 'undefined'
typeof Symbol()    // 'symbol'
typeof BigInt(1)   // 'bigint'
typeof function(){} // 'function'

// 以下全部都是 'object'
typeof {}          // 'object'
typeof []          // 'object'
typeof null        // 'object' ← 這是 JavaScript 的歷史 bug
typeof new Date()  // 'object'
typeof /regex/     // 'object'

這在 TypeScript 中會造成問題,因為我們需要更精確的型別判斷。


Type Guards 是什麼?

Type Guards 是一種讓 TypeScript 編譯器在特定程式碼區塊中「縮小」型別範圍的技術。

function processValue(value: string | number) {
  // 這裡 value 的型別是 string | number

  if (typeof value === 'string') {
    // 這裡 value 的型別被「縮小」為 string
    console.log(value.toUpperCase()); // ✅ TypeScript 知道這是 string
  } else {
    // 這裡 value 的型別被「縮小」為 number
    console.log(value.toFixed(2)); // ✅ TypeScript 知道這是 number
  }
}

四種內建 Type Guards

1. typeof Guard

適用於 Primitive Types(string, number, boolean, symbol, bigint, undefined)

function padLeft(value: string, padding: string | number): string {
  if (typeof padding === 'number') {
    // padding: number
    return ' '.repeat(padding) + value;
  }
  // padding: string
  return padding + value;
}

console.log(padLeft('Hello', 4));      // '    Hello'
console.log(padLeft('Hello', '>>> ')); // '>>> Hello'

typeof 可以判斷的值:

  • 'string'

  • 'number'

  • 'boolean'

  • 'symbol'

  • 'bigint'

  • 'undefined'

  • 'function'

  • 'object'(包含 null、array、object、Date 等)

2. instanceof Guard

適用於 Class 實例

class Dog {
  bark() {
    console.log('Woof!');
  }
}

class Cat {
  meow() {
    console.log('Meow!');
  }
}

function makeSound(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    // animal: Dog
    animal.bark();
  } else {
    // animal: Cat
    animal.meow();
  }
}

// 也適用於內建類別
function processDate(value: Date | string) {
  if (value instanceof Date) {
    // value: Date
    console.log(value.getFullYear());
  } else {
    // value: string
    console.log(new Date(value).getFullYear());
  }
}

3. in Operator Guard

適用於 檢查屬性是否存在

interface Fish {
  swim: () => void;
}

interface Bird {
  fly: () => void;
}

function move(animal: Fish | Bird) {
  if ('swim' in animal) {
    // animal: Fish
    animal.swim();
  } else {
    // animal: Bird
    animal.fly();
  }
}

// 實際應用:API Response 處理
interface SuccessResponse {
  data: any;
  status: 'success';
}

interface ErrorResponse {
  error: string;
  status: 'error';
}

function handleResponse(response: SuccessResponse | ErrorResponse) {
  if ('data' in response) {
    // response: SuccessResponse
    console.log('Data:', response.data);
  } else {
    // response: ErrorResponse
    console.log('Error:', response.error);
  }
}

4. 真值檢查(Truthiness Narrowing)

function printLength(str: string | null | undefined) {
  if (str) {
    // str: string(排除了 null 和 undefined)
    console.log(str.length);
  } else {
    console.log('No string provided');
  }
}

// 注意:這會排除空字串 ''
function printLengthSafe(str: string | null | undefined) {
  if (str !== null && str !== undefined) {
    // str: string(包含空字串)
    console.log(str.length);
  }
}

自定義 Type Guards(Type Predicates)

當內建的 Type Guards 不夠用時,我們可以用 is 關鍵字建立自定義 Type Guard。

基本語法

function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function processValue(value: unknown) {
  if (isString(value)) {
    // value: string
    console.log(value.toUpperCase());
  }
}

實際應用:檢查陣列

// 檢查是否為陣列
function isArray<T>(value: unknown): value is T[] {
  return Array.isArray(value);
}

// 檢查是否為字串陣列
function isStringArray(value: unknown): value is string[] {
  return Array.isArray(value) && value.every(item => typeof item === 'string');
}

function processData(data: unknown) {
  if (isStringArray(data)) {
    // data: string[]
    data.forEach(str => console.log(str.toUpperCase()));
  }
}

實際應用:區分 Interface

interface Car {
  type: 'car';
  brand: string;
  wheels: 4;
}

interface Motorcycle {
  type: 'motorcycle';
  brand: string;
  wheels: 2;
}

type Vehicle = Car | Motorcycle;

// 自定義 Type Guard
function isCar(vehicle: Vehicle): vehicle is Car {
  return vehicle.type === 'car';
}

function isMotorcycle(vehicle: Vehicle): vehicle is Motorcycle {
  return vehicle.type === 'motorcycle';
}

function describeVehicle(vehicle: Vehicle) {
  if (isCar(vehicle)) {
    console.log(`${vehicle.brand} car with ${vehicle.wheels} wheels`);
  } else {
    console.log(`${vehicle.brand} motorcycle with ${vehicle.wheels} wheels`);
  }
}

Discriminated Unions(可辨識聯合)

這是 TypeScript 最強大的模式之一!

基本概念

// 每個型別都有一個「discriminant」屬性(這裡是 type)
interface Circle {
  type: 'circle';
  radius: number;
}

interface Rectangle {
  type: 'rectangle';
  width: number;
  height: number;
}

interface Triangle {
  type: 'triangle';
  base: number;
  height: number;
}

type Shape = Circle | Rectangle | Triangle;

function calculateArea(shape: Shape): number {
  switch (shape.type) {
    case 'circle':
      // shape: Circle
      return Math.PI * shape.radius ** 2;

    case 'rectangle':
      // shape: Rectangle
      return shape.width * shape.height;

    case 'triangle':
      // shape: Triangle
      return (shape.base * shape.height) / 2;
  }
}

Exhaustive Checking(窮舉檢查)

確保你處理了所有可能的 case:

function calculateArea(shape: Shape): number {
  switch (shape.type) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      return shape.width * shape.height;
    case 'triangle':
      return (shape.base * shape.height) / 2;
    default:
      // 如果有未處理的 case,這裡會報錯
      const _exhaustiveCheck: never = shape;
      throw new Error(`Unhandled shape: ${_exhaustiveCheck}`);
  }
}

如果之後新增了一個 Square 型別但忘記處理,TypeScript 會在編譯時報錯!

實際應用:Redux Action

interface AddTodoAction {
  type: 'ADD_TODO';
  payload: { text: string };
}

interface RemoveTodoAction {
  type: 'REMOVE_TODO';
  payload: { id: number };
}

interface ToggleTodoAction {
  type: 'TOGGLE_TODO';
  payload: { id: number };
}

type TodoAction = AddTodoAction | RemoveTodoAction | ToggleTodoAction;

function todoReducer(state: Todo[], action: TodoAction): Todo[] {
  switch (action.type) {
    case 'ADD_TODO':
      // action.payload: { text: string }
      return [...state, { id: Date.now(), text: action.payload.text, done: false }];

    case 'REMOVE_TODO':
      // action.payload: { id: number }
      return state.filter(todo => todo.id !== action.payload.id);

    case 'TOGGLE_TODO':
      // action.payload: { id: number }
      return state.map(todo =>
        todo.id === action.payload.id ? { ...todo, done: !todo.done } : todo
      );

    default:
      const _exhaustiveCheck: never = action;
      return state;
  }
}

處理 null 和 undefined

使用 Optional Chaining 和 Nullish Coalescing

interface User {
  name: string;
  address?: {
    city?: string;
  };
}

function getCity(user: User | null | undefined): string {
  // Optional chaining (?.)
  const city = user?.address?.city;

  // Nullish coalescing (??)
  return city ?? 'Unknown';
}

// 等同於
function getCityVerbose(user: User | null | undefined): string {
  if (user === null || user === undefined) {
    return 'Unknown';
  }
  if (user.address === undefined) {
    return 'Unknown';
  }
  if (user.address.city === undefined) {
    return 'Unknown';
  }
  return user.address.city;
}

非空斷言(謹慎使用)

function processUser(user: User | null) {
  // 你確定 user 不是 null 時可以使用 !
  console.log(user!.name); // ⚠️ 如果 user 是 null 會在執行時報錯

  // 更好的做法
  if (user) {
    console.log(user.name);
  }
}

常見錯誤模式

錯誤 1:typeof 判斷陣列

// ❌ 錯誤
function processArray(value: unknown) {
  if (typeof value === 'array') { // 'array' 不是有效的 typeof 結果!
    // ...
  }
}

// ✅ 正確
function processArray(value: unknown) {
  if (Array.isArray(value)) {
    // value: unknown[] 或更具體的型別
  }
}

錯誤 2:忘記 typeof null 是 'object'

// ❌ 錯誤:null 也會通過這個檢查
function processObject(value: unknown) {
  if (typeof value === 'object') {
    console.log(value.toString()); // 如果 value 是 null 會報錯!
  }
}

// ✅ 正確
function processObject(value: unknown) {
  if (typeof value === 'object' && value !== null) {
    console.log(value.toString());
  }
}

錯誤 3:Type Predicate 邏輯錯誤

interface Cat {
  meow: () => void;
}

interface Dog {
  bark: () => void;
}

// ❌ 錯誤:回傳 true 但實際不是 Cat
function isCat(animal: Cat | Dog): animal is Cat {
  return true; // 永遠回傳 true,邏輯錯誤!
}

// ✅ 正確
function isCat(animal: Cat | Dog): animal is Cat {
  return 'meow' in animal;
}

最佳實踐

1. 優先使用 Discriminated Unions

// ✅ 好:使用 discriminant
type Result<T> =
  | { success: true; data: T }
  | { success: false; error: string };

function handleResult<T>(result: Result<T>) {
  if (result.success) {
    console.log(result.data);
  } else {
    console.log(result.error);
  }
}

2. 保持 Type Guard 簡單

// ✅ 好:簡單明確
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

// ❌ 壞:太複雜,難以維護
function isValidUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'name' in value &&
    typeof (value as any).name === 'string' &&
    'age' in value &&
    typeof (value as any).age === 'number' &&
    (value as any).age >= 0
    // ... 更多檢查
  );
}

// ✅ 好:使用 Zod 或類似的 runtime validation 庫
import { z } from 'zod';
const UserSchema = z.object({
  name: z.string(),
  age: z.number().nonnegative(),
});

3. 使用 Assertion Functions(TypeScript 3.7+)

function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error('Expected a string');
  }
}

function processValue(value: unknown) {
  assertIsString(value);
  // 這之後 value 的型別就是 string
  console.log(value.toUpperCase());
}

常見面試題

題目 1:typeof 可能的回傳值有哪些?

答案

7 種可能的值:

  • 'string'

  • 'number'

  • 'boolean'

  • 'undefined'

  • 'object'(包含 null、array、object)

  • 'function'

  • 'symbol'

  • 'bigint'(ES2020+)

題目 2:如何正確檢查一個值是否為陣列?

答案

// ✅ 使用 Array.isArray()
if (Array.isArray(value)) {
  // value is an array
}

// ❌ typeof 無法區分
if (typeof value === 'object') {
  // 可能是 object、array、或 null
}

題目 3:寫一個 Type Guard 來區分 Success 和 Error Response

答案

interface SuccessResponse<T> {
  status: 'success';
  data: T;
}

interface ErrorResponse {
  status: 'error';
  message: string;
}

type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;

// 方法 1:使用 in operator
function isSuccess<T>(response: ApiResponse<T>): response is SuccessResponse<T> {
  return 'data' in response;
}

// 方法 2:使用 discriminant
function isSuccessV2<T>(response: ApiResponse<T>): response is SuccessResponse<T> {
  return response.status === 'success';
}


總結

  1. typeof 只能辨識 7 種型別,無法區分 object、array、null

  2. instanceof 用於檢查 class 實例

  3. in operator 用於檢查屬性是否存在

  4. 自定義 Type Guards 使用 value is Type 語法

  5. Discriminated Unions 是最強大的型別安全模式

  6. Exhaustive Checkingnever 確保處理所有 case

  7. 記得 typeof null === 'object' 是 JavaScript 的歷史 bug!


延伸閱讀

Enjoyed this article? Show some love!

0
Clap

Enjoyed this article?

Subscribe for engineering notes and AI development insights

We respect your privacy. No spam, unsubscribe anytime.

Share this article

Comments