TypeScript 型別註解 (Type Annotations)

型別註解 (Type Annotations) 是 TypeScript 最核心的功能之一。透過在程式碼中加入型別標記,我們可以明確指定變數、參數和回傳值的型別,讓 TypeScript 編譯器進行型別檢查。

變數型別註解

在變數名稱後加上 : 型別 來標註型別:

// 基本型別
let name: string = '小明';
let age: number = 25;
let isActive: boolean = true;

// 陣列
let numbers: number[] = [1, 2, 3];
let names: string[] = ['Alice', 'Bob'];

// 物件
let user: { name: string; age: number } = {
  name: '小明',
  age: 25,
};

函式型別註解

參數型別

在參數名稱後加上型別:

function greet(name: string) {
  console.log(`Hello, ${name}!`);
}

greet('小明'); // OK
// greet(123);    // 錯誤:型別 'number' 的引數不可指派給型別 'string' 的參數

回傳值型別

在參數列表後加上回傳型別:

function add(a: number, b: number): number {
  return a + b;
}

function greet(name: string): string {
  return `Hello, ${name}!`;
}

function log(message: string): void {
  console.log(message);
  // 沒有回傳值
}

箭頭函式

// 完整標註
const add: (a: number, b: number) => number = (a, b) => a + b;

// 只標註參數,讓 TypeScript 推論回傳型別
const multiply = (a: number, b: number) => a * b;

// 明確標註回傳型別
const divide = (a: number, b: number): number => a / b;

可選參數與預設值

可選參數

使用 ? 標記可選參數:

function greet(name: string, greeting?: string) {
  if (greeting) {
    console.log(`${greeting}, ${name}!`);
  } else {
    console.log(`Hello, ${name}!`);
  }
}

greet('小明'); // Hello, 小明!
greet('小明', '早安'); // 早安, 小明!
可選參數的型別會自動變成 型別 | undefined,所以上例中 greeting 的型別是 string | undefined

預設參數

function greet(name: string, greeting: string = 'Hello') {
  console.log(`${greeting}, ${name}!`);
}

greet('小明'); // Hello, 小明!
greet('小明', 'Hi'); // Hi, 小明!

有預設值的參數不需要加 ?,TypeScript 會自動推論它是可選的。

物件型別註解

內聯物件型別

function printUser(user: { name: string; age: number; email?: string }) {
  console.log(`${user.name}, ${user.age} 歲`);
  if (user.email) {
    console.log(`Email: ${user.email}`);
  }
}

printUser({ name: '小明', age: 25 });
printUser({ name: '小華', age: 30, email: 'hua@example.com' });

唯讀屬性

使用 readonly 標記唯讀屬性:

function printPoint(point: { readonly x: number; readonly y: number }) {
  console.log(`(${point.x}, ${point.y})`);
  // point.x = 10;  // 錯誤:無法指派給 'x',因為它是唯讀屬性
}

函式型別

函式本身也可以作為型別:

// 定義函式型別
type MathOperation = (a: number, b: number) => number;

// 使用函式型別
let add: MathOperation = (a, b) => a + b;
let subtract: MathOperation = (a, b) => a - b;

// 作為參數
function calculate(a: number, b: number, operation: MathOperation): number {
  return operation(a, b);
}

console.log(calculate(10, 5, add)); // 15
console.log(calculate(10, 5, subtract)); // 5

回呼函式型別

// 定義接受回呼函式的函式
function fetchData(url: string, callback: (data: string) => void) {
  // 模擬非同步操作
  setTimeout(() => {
    callback('資料內容');
  }, 1000);
}

fetchData('https://api.example.com', (data) => {
  console.log(data);
});

剩餘參數 (Rest Parameters)

function sum(...numbers: number[]): number {
  return numbers.reduce((acc, n) => acc + n, 0);
}

console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15

this 的型別

在某些情況下,需要明確標註 this 的型別:

interface User {
  name: string;
  greet(this: User): void;
}

const user: User = {
  name: '小明',
  greet() {
    console.log(`Hello, I'm ${this.name}`);
  },
};

user.greet(); // Hello, I'm 小明

// const greet = user.greet;
// greet();  // 錯誤:'this' 的型別不正確

函式重載 (Function Overloads)

TypeScript 允許定義多個函式簽名:

// 重載簽名
function format(value: string): string;
function format(value: number): string;
function format(value: Date): string;

// 實作簽名
function format(value: string | number | Date): string {
  if (typeof value === 'string') {
    return value.toUpperCase();
  } else if (typeof value === 'number') {
    return value.toFixed(2);
  } else {
    return value.toISOString();
  }
}

console.log(format('hello')); // HELLO
console.log(format(3.14159)); // 3.14
console.log(format(new Date())); // 2024-12-10T...

型別斷言 (Type Assertion)

當你比 TypeScript 更清楚值的型別時,可以使用型別斷言:

// 使用 as 語法 (推薦)
let value: unknown = 'hello';
let length: number = (value as string).length;

// 使用角括號語法 (在 JSX 中不能使用)
let length2: number = (<string>value).length;
型別斷言只是告訴編譯器「我知道這個值的型別」,不會進行任何型別轉換。如果斷言錯誤,執行時可能會出錯。

常見錯誤與最佳實踐

避免過度標註

TypeScript 有強大的型別推論,不需要每個地方都加上型別:

// 不必要的標註
let name: string = '小明'; // TypeScript 可以推論出 string

// 只在需要時標註
let name = '小明'; // 自動推論為 string

函式回傳值建議標註

雖然可以推論,但明確標註回傳型別有助於:

  • 文件化函式的意圖
  • 及早發現實作錯誤
// 明確的回傳型別有助於發現錯誤
function getUser(id: number): { name: string; age: number } {
  // 如果忘記回傳 age,編譯器會報錯
  return { name: '小明', age: 25 };
}

避免使用 any

// 不好:使用 any
function process(data: any) {
  return data.foo.bar; // 沒有型別檢查
}

// 好:使用具體型別或 unknown
function process(data: { foo: { bar: string } }) {
  return data.foo.bar; // 有型別檢查
}