TypeScript 型別推論 (Type Inference)

型別推論 (Type Inference) 是 TypeScript 自動判斷變數型別的能力。即使你沒有明確標註型別,TypeScript 也能根據程式碼的上下文推論出正確的型別。

基本型別推論

當你初始化變數時,TypeScript 會根據初始值推論型別:

// TypeScript 自動推論這些變數的型別
let name = '小明'; // 推論為 string
let age = 25; // 推論為 number
let isActive = true; // 推論為 boolean
let nothing = null; // 推論為 null
let notDefined; // 推論為 any (沒有初始值)

在 VS Code 或其他 IDE 中,將滑鼠移到變數上可以看到推論的型別。

函式回傳型別推論

TypeScript 會根據 return 語句推論函式的回傳型別:

// 推論回傳型別為 number
function add(a: number, b: number) {
  return a + b;
}

// 推論回傳型別為 string
function greet(name: string) {
  return `Hello, ${name}!`;
}

// 推論回傳型別為 void (沒有 return)
function log(message: string) {
  console.log(message);
}

// 推論回傳型別為 number | undefined
function maybeNumber(flag: boolean) {
  if (flag) {
    return 42;
  }
  // 沒有 else,隱含回傳 undefined
}

最佳共同型別 (Best Common Type)

當推論多個表達式的型別時,TypeScript 會找出最佳共同型別:

// 推論為 (number | string)[]
let mixed = [1, 'hello', 2, 'world'];

// 推論為 number[]
let numbers = [1, 2, 3, 4, 5];

// 推論為 (number | null)[]
let maybeNumbers = [1, null, 2, null, 3];

物件陣列的推論

class Animal {
  name: string = '';
}
class Dog extends Animal {
  breed: string = '';
}
class Cat extends Animal {
  color: string = '';
}

// 推論為 (Dog | Cat)[],不是 Animal[]
let pets = [new Dog(), new Cat()];

// 如果要推論為 Animal[],需要明確標註
let animals: Animal[] = [new Dog(), new Cat()];

上下文型別 (Contextual Typing)

TypeScript 也會根據表達式所在的位置來推論型別:

事件處理器

// TypeScript 知道 event 是 MouseEvent
document.addEventListener('click', function (event) {
  console.log(event.clientX, event.clientY); // OK
});

// 箭頭函式也一樣
window.onmousemove = (event) => {
  console.log(event.x, event.y); // OK
};

回呼函式

let numbers = [1, 2, 3, 4, 5];

// TypeScript 推論 n 是 number
numbers.forEach((n) => {
  console.log(n.toFixed(2)); // OK
});

// map 的回呼也會自動推論
let doubled = numbers.map((n) => n * 2); // doubled 是 number[]

物件方法

interface User {
  name: string;
  greet: (greeting: string) => void;
}

// 實作時,greeting 自動被推論為 string
const user: User = {
  name: '小明',
  greet(greeting) {
    console.log(`${greeting}, I'm ${this.name}`);
  },
};

let vs const 的推論差異

letconst 宣告的變數有不同的型別推論:

// let 推論為較寬的型別
let message = 'hello'; // 型別是 string

// const 推論為字面量型別
const greeting = 'hello'; // 型別是 "hello" (字面量型別)

// 物件的情況
const user = {
  name: '小明',
  age: 25,
};
// user 的型別是 { name: string; age: number }
// 不是 { name: "小明"; age: 25 }

as const 斷言

使用 as const 可以將物件推論為更精確的字面量型別:

const config = {
  api: 'https://api.example.com',
  timeout: 5000,
} as const;
// 型別是 { readonly api: "https://api.example.com"; readonly timeout: 5000 }

const colors = ['red', 'green', 'blue'] as const;
// 型別是 readonly ["red", "green", "blue"]

控制流程分析 (Control Flow Analysis)

TypeScript 會追蹤程式的控制流程來收窄型別:

function example(value: string | number) {
  // 這裡 value 是 string | number

  if (typeof value === 'string') {
    // 這裡 value 被收窄為 string
    console.log(value.toUpperCase());
  } else {
    // 這裡 value 被收窄為 number
    console.log(value.toFixed(2));
  }
}

真值檢查

function printName(name: string | null | undefined) {
  if (name) {
    // name 被收窄為 string
    console.log(name.toUpperCase());
  }
}

可辨識聯合 (Discriminated Unions)

interface Circle {
  kind: 'circle';
  radius: number;
}

interface Square {
  kind: 'square';
  side: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      // shape 被收窄為 Circle
      return Math.PI * shape.radius ** 2;
    case 'square':
      // shape 被收窄為 Square
      return shape.side ** 2;
  }
}

型別推論的限制

沒有初始值的變數

let value; // 推論為 any
value = 'hello';
value = 123; // 都可以,因為是 any

空陣列

let items = []; // 推論為 any[] 或 never[]

// 需要明確標註
let numbers: number[] = [];
numbers.push(1); // OK

最佳實踐

何時依賴推論

// 簡單的初始化 - 依賴推論
let name = '小明';
let count = 0;
let items = [1, 2, 3];

// 明顯的函式回傳 - 依賴推論
const double = (n: number) => n * 2;

何時明確標註

// 函式參數 - 通常需要標註
function greet(name: string): string {
  return `Hello, ${name}`;
}

// 空陣列 - 需要標註
let users: User[] = [];

// 複雜的物件 - 建議標註
interface Config {
  api: string;
  timeout: number;
}
const config: Config = {
  api: 'https://api.example.com',
  timeout: 5000,
};

// 公開的 API - 明確標註回傳型別
export function fetchUser(id: number): Promise<User> {
  // ...
}

satisfies 運算子 (TypeScript 4.9+)

satisfies 讓你既能享受型別推論,又能確保型別安全:

type Colors = Record<string, [number, number, number] | string>;

// 使用 satisfies 保留精確的推論型別
const palette = {
  red: [255, 0, 0],
  green: '#00ff00',
  blue: [0, 0, 255],
} satisfies Colors;

// palette.red 的型別是 [number, number, number],不是 [number, number, number] | string
const redComponent = palette.red[0]; // OK

// palette.green 的型別是 string
const greenHex = palette.green.toUpperCase(); // OK