TypeScript 型別守衛 (Type Guards)
型別守衛 (Type Guards) 是用來在執行時期檢查型別,讓 TypeScript 能夠收窄變數的型別。除了內建的 typeof、instanceof 和 in 運算子外,我們還可以建立自訂的型別守衛。
自訂型別守衛函式
使用 is 關鍵字定義型別謂詞 (type predicate):
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
// 自訂型別守衛
function isFish(animal: Bird | Fish): animal is Fish {
return (animal as Fish).swim !== undefined;
}
function move(animal: Bird | Fish) {
if (isFish(animal)) {
// animal 被收窄為 Fish
animal.swim();
} else {
// animal 被收窄為 Bird
animal.fly();
}
}
型別謂詞語法
function isType(value: unknown): value is SpecificType {
// 回傳 boolean,但 TypeScript 會根據結果收窄型別
return /* 型別檢查邏輯 */;
}
value是要檢查的參數value is SpecificType是型別謂詞- 函式必須回傳
boolean
常見的型別守衛模式
檢查 null 和 undefined
function isNotNull<T>(value: T | null): value is T {
return value !== null;
}
function isNotUndefined<T>(value: T | undefined): value is T {
return value !== undefined;
}
function isDefined<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
// 使用
const items = ['a', null, 'b', undefined, 'c'];
const validItems = items.filter(isDefined); // string[]
檢查陣列
function isArray<T>(value: T | T[]): value is T[] {
return Array.isArray(value);
}
function isNonEmptyArray<T>(value: T[]): value is [T, ...T[]] {
return value.length > 0;
}
// 使用
function process(value: string | string[]) {
if (isArray(value)) {
value.forEach((item) => console.log(item));
} else {
console.log(value);
}
}
檢查物件屬性
function hasProperty<T extends object, K extends string>(
obj: T,
key: K
): obj is T & Record<K, unknown> {
return key in obj;
}
// 使用
function process(obj: object) {
if (hasProperty(obj, 'name')) {
console.log(obj.name); // OK
}
}
類別的型別守衛
class Cat {
meow() {
console.log('喵~');
}
}
class Dog {
bark() {
console.log('汪!');
}
}
function isCat(animal: Cat | Dog): animal is Cat {
return animal instanceof Cat;
}
function isDog(animal: Cat | Dog): animal is Dog {
return animal instanceof Dog;
}
function makeSound(animal: Cat | Dog) {
if (isCat(animal)) {
animal.meow();
} else {
animal.bark();
}
}
可辨識聯合的型別守衛
interface Circle {
kind: 'circle';
radius: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
type Shape = Circle | Rectangle;
function isCircle(shape: Shape): shape is Circle {
return shape.kind === 'circle';
}
function isRectangle(shape: Shape): shape is Rectangle {
return shape.kind === 'rectangle';
}
// 泛型版本
function isOfKind<T extends Shape, K extends T['kind']>(
shape: T,
kind: K
): shape is Extract<T, { kind: K }> {
return shape.kind === kind;
}
斷言函式 (Assertion Functions)
TypeScript 3.7+ 支援斷言函式,用 asserts 關鍵字:
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error(`Expected string, got ${typeof value}`);
}
}
function assertIsDefined<T>(value: T | null | undefined): asserts value is T {
if (value === null || value === undefined) {
throw new Error('Value is null or undefined');
}
}
// 使用
function process(value: unknown) {
assertIsString(value);
// 如果沒有拋出例外,value 被收窄為 string
console.log(value.toUpperCase());
}
條件斷言
function assert(condition: boolean, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
function process(value: string | null) {
assert(value !== null, 'Value cannot be null');
// value 被收窄為 string
console.log(value.length);
}
泛型型別守衛
// 檢查值是否為特定型別
function isOfType<T>(value: unknown, check: (value: unknown) => boolean): value is T {
return check(value);
}
// 檢查物件是否符合介面
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
// 建立型別守衛工廠
function createTypeGuard<T>(check: (value: unknown) => boolean): (value: unknown) => value is T {
return (value: unknown): value is T => check(value);
}
const isNumber = createTypeGuard<number>((v) => typeof v === 'number');
const isString = createTypeGuard<string>((v) => typeof v === 'string');
組合型別守衛
// AND 組合
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((item) => typeof item === 'string');
}
// OR 組合
function isStringOrNumber(value: unknown): value is string | number {
return typeof value === 'string' || typeof value === 'number';
}
// 複雜物件
interface User {
id: number;
name: string;
email: string;
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value &&
typeof (value as User).id === 'number' &&
typeof (value as User).name === 'string' &&
typeof (value as User).email === 'string'
);
}
實用範例
API 資料驗證
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
}
function isSuccessResponse<T>(
response: ApiResponse<T>
): response is ApiResponse<T> & { success: true; data: T } {
return response.success === true && response.data !== undefined;
}
function isErrorResponse<T>(
response: ApiResponse<T>
): response is ApiResponse<T> & { success: false; error: string } {
return response.success === false && response.error !== undefined;
}
async function fetchUser(): Promise<ApiResponse<User>> {
// ...
}
async function handleFetch() {
const response = await fetchUser();
if (isSuccessResponse(response)) {
console.log(response.data.name); // 安全存取
} else if (isErrorResponse(response)) {
console.error(response.error); // 安全存取
}
}
表單資料處理
interface TextInput {
type: 'text';
value: string;
maxLength?: number;
}
interface NumberInput {
type: 'number';
value: number;
min?: number;
max?: number;
}
interface SelectInput {
type: 'select';
value: string;
options: string[];
}
type FormInput = TextInput | NumberInput | SelectInput;
function isTextInput(input: FormInput): input is TextInput {
return input.type === 'text';
}
function isNumberInput(input: FormInput): input is NumberInput {
return input.type === 'number';
}
function isSelectInput(input: FormInput): input is SelectInput {
return input.type === 'select';
}
function validateInput(input: FormInput): boolean {
if (isTextInput(input)) {
if (input.maxLength && input.value.length > input.maxLength) {
return false;
}
} else if (isNumberInput(input)) {
if (input.min !== undefined && input.value < input.min) {
return false;
}
if (input.max !== undefined && input.value > input.max) {
return false;
}
}
return true;
}
錯誤處理
class ValidationError extends Error {
constructor(
public field: string,
message: string
) {
super(message);
this.name = 'ValidationError';
}
}
class NetworkError extends Error {
constructor(
public statusCode: number,
message: string
) {
super(message);
this.name = 'NetworkError';
}
}
function isValidationError(error: Error): error is ValidationError {
return error instanceof ValidationError;
}
function isNetworkError(error: Error): error is NetworkError {
return error instanceof NetworkError;
}
function handleError(error: Error) {
if (isValidationError(error)) {
console.log(`驗證錯誤 - 欄位: ${error.field}, 訊息: ${error.message}`);
} else if (isNetworkError(error)) {
console.log(`網路錯誤 - 狀態碼: ${error.statusCode}`);
} else {
console.log(`未知錯誤: ${error.message}`);
}
}