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驗證型別並保留推論
雙重斷言繞過型別檢查非常高