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 Checking 用 never 確保處理所有 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