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 的推論差異
let 和 const 宣告的變數有不同的型別推論:
// 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