TypeScript 型別斷言 (Type Assertion)
型別斷言 (Type Assertion) 是一種告訴 TypeScript 編譯器「我比你更了解這個值的型別」的方式。它不會進行任何型別轉換,只是在編譯時期讓 TypeScript 將值視為指定的型別。
基本語法
TypeScript 提供兩種型別斷言的語法:
as 語法 (推薦)
let value: unknown = 'hello';
let length: number = (value as string).length;
// 也可以寫在單獨的語句
let str = value as string;
console.log(str.toUpperCase());
角括號語法
let value: unknown = 'hello';
let length: number = (<string>value).length;
在 .jsx / .tsx 檔案中,角括號語法會與 JSX 標籤衝突,所以建議統一使用
as 語法。常見使用場景
DOM 元素
// getElementById 回傳 HTMLElement | null
const input = document.getElementById('username');
// 斷言為更具體的型別
const inputElement = document.getElementById('username') as HTMLInputElement;
inputElement.value = 'hello';
// querySelector 也類似
const button = document.querySelector('.submit-btn') as HTMLButtonElement;
button.disabled = true;
處理 null
// 不使用斷言,需要檢查 null
const element = document.getElementById('app');
if (element) {
element.innerHTML = 'Hello';
}
// 使用斷言(確定元素存在時)
const element2 = document.getElementById('app') as HTMLElement;
element2.innerHTML = 'Hello';
API 回應
interface User {
id: number;
name: string;
email: string;
}
async function fetchUser(): Promise<User> {
const response = await fetch('/api/user');
const data = await response.json();
return data as User;
}
any 型別轉換
// 將 any 轉換為具體型別
function processData(data: any): string {
const result = data as { message: string };
return result.message;
}
非空斷言 (Non-null Assertion)
使用 ! 後綴告訴 TypeScript 某個值一定不是 null 或 undefined:
function getValue(): string | null {
return 'hello';
}
// 非空斷言
let value = getValue()!;
console.log(value.length); // OK,不會警告可能為 null
// 常見於 DOM 操作
const element = document.getElementById('app')!;
element.innerHTML = 'Hello'; // 不需要額外檢查 null
非空斷言具有風險,如果值實際上是 null 或 undefined,執行時會出錯。只在你確定值不為空時使用。
const 斷言
使用 as const 將值轉換為最精確的字面量型別:
// 一般變數
let point = { x: 10, y: 20 };
// 型別:{ x: number; y: number }
// const 斷言
let point2 = { x: 10, y: 20 } as const;
// 型別:{ readonly x: 10; readonly y: 20 }
// 陣列
let colors = ['red', 'green', 'blue'];
// 型別:string[]
let colors2 = ['red', 'green', 'blue'] as const;
// 型別:readonly ["red", "green", "blue"]
從 const 斷言提取型別
const ACTIONS = ['create', 'read', 'update', 'delete'] as const;
type Action = (typeof ACTIONS)[number];
// "create" | "read" | "update" | "delete"
const STATUS = {
PENDING: 'pending',
ACTIVE: 'active',
COMPLETED: 'completed',
} as const;
type Status = (typeof STATUS)[keyof typeof STATUS];
// "pending" | "active" | "completed"
雙重斷言
當直接斷言不被允許時,可以使用雙重斷言:
// 這是錯誤的
// let num = "hello" as number; // 錯誤
// 雙重斷言(不推薦,除非必要)
let num = 'hello' as unknown as number;
雙重斷言非常危險,它完全繞過了型別系統的檢查。只在極端情況下使用,並確保你知道自己在做什麼。
satisfies 運算子
TypeScript 4.9+ 引入的 satisfies 運算子,既能驗證型別又能保留推論:
type Color = Record<string, [number, number, number] | string>;
// 使用 satisfies 保留精確型別
const palette = {
red: [255, 0, 0],
green: '#00ff00',
blue: [0, 0, 255],
} satisfies Color;
// palette.red 的型別是 [number, number, number],不是 [number, number, number] | string
palette.red[0]; // OK
// palette.green 的型別是 string
palette.green.toUpperCase(); // OK
satisfies vs as
// 使用 as
const config1 = {
port: 3000,
host: 'localhost',
} as Record<string, string | number>;
// config1.port 是 string | number
// 使用 satisfies
const config2 = {
port: 3000,
host: 'localhost',
} satisfies Record<string, string | number>;
// config2.port 是 number
// config2.host 是 string
型別斷言的限制
TypeScript 不允許完全不相關的型別之間的斷言:
// 允許
let str: string = 'hello';
let unknown1: unknown = str;
let any1: any = str;
// 不允許(沒有重疊)
// let num: number = "hello" as number; // 錯誤
// 需要雙重斷言
let num: number = 'hello' as unknown as number;
最佳實踐
優先使用型別守衛
// 不好:使用斷言
function process(value: unknown) {
const str = value as string;
console.log(str.toUpperCase()); // 可能執行時出錯
}
// 好:使用型別守衛
function process(value: unknown) {
if (typeof value === 'string') {
console.log(value.toUpperCase()); // 安全
}
}
縮小斷言範圍
// 不好:整個物件斷言
const data = response as { user: User; posts: Post[] };
// 好:只在需要時斷言
const user = response.user as User;
const posts = response.posts as Post[];
使用型別謂詞
// 建立型別守衛函式
function isUser(data: unknown): data is User {
return typeof data === 'object' && data !== null && 'id' in data && 'name' in data;
}
// 使用型別守衛
function processData(data: unknown) {
if (isUser(data)) {
console.log(data.name); // 安全
}
}
實用範例
表單處理
function handleSubmit(event: Event) {
event.preventDefault();
const form = event.target as HTMLFormElement;
const formData = new FormData(form);
const nameInput = form.elements.namedItem('name') as HTMLInputElement;
console.log(nameInput.value);
}
JSON 解析
interface Config {
apiUrl: string;
timeout: number;
debug: boolean;
}
function loadConfig(json: string): Config {
const data = JSON.parse(json);
// 驗證必要欄位
if (!data.apiUrl || typeof data.timeout !== 'number') {
throw new Error('Invalid config');
}
return data as Config;
}
事件處理
function handleClick(event: MouseEvent) {
const target = event.target as HTMLElement;
const button = event.currentTarget as HTMLButtonElement;
target.classList.add('clicked');
button.disabled = true;
}
泛型與斷言
function createInstance<T>(Constructor: new () => T): T {
return new Constructor() as T;
}
function clone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj)) as T;
}
總結
| 方式 | 用途 | 風險 |
|---|---|---|
as Type | 一般型別斷言 | 中等 |
<Type> | 一般型別斷言(JSX 不可用) | 中等 |
value! | 非空斷言 | 高 |
as const | 字面量型別斷言 | 低 |
satisfies | 驗證型別並保留推論 | 低 |
| 雙重斷言 | 繞過型別檢查 | 非常高 |