前言:一個讓人困惑的面試題
面試官:「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';
}
總結
typeof 只能辨識 7 種型別,無法區分 object、array、null
instanceof 用於檢查 class 實例
in operator 用於檢查屬性是否存在
自定義 Type Guards 使用
value is Type語法Discriminated Unions 是最強大的型別安全模式
Exhaustive Checking 用
never確保處理所有 case記得
typeof null === 'object'是 JavaScript 的歷史 bug!