TypeScript 型別推論 (Type Inference)
很多初學者有一個誤解:「TypeScript 就是要一直寫 : string, : number 很麻煩」。
錯!大錯特錯!
TypeScript 的編譯器非常聰明(而且很懶),如果你不告訴它型別,它會試著從你的程式碼中去「猜」出型別。這個「猜」的過程,就叫做型別推論 (Type Inference)。
自動推論 (Automatic Inference)
變數初始化
當你有給初始值時,TS 會自動鎖定型別。
let x = 3;
// TS os: 你把它設為 3,那 x 肯定是 number 沒錯吧?
// x = "hello";
// 報錯:你剛剛不是說是 number 嗎?怎麼現在變卦了?
最佳共同型別 (Best Common Type)
當你初始化一個陣列,裡面有好幾種不同型別時,TS 會試著找出一個「能涵蓋所有元素」的型別。
let items = [0, 1, null, 'hello'];
// TS 推論結果:(string | number | null)[]
// 意思是:這個陣列裡裝的「要嘛是字串,要嘛數字,不然就是 null」
上下文推論 (Contextual Typing)
這時候 TS 是根據「變數的位置」來推論型別的。最常見的就是事件監聽器。
// 因為這是 "click" 事件,TS 知道 callback 的第一個參數一定是 MouseEvent
window.onmousedown = function (mouseEvent) {
console.log(mouseEvent.button); // OK
console.log(mouseEvent.kangaroo); // 報錯:MouseEvent 沒有 kangaroo 屬性
};
如果你把上面的 onmousedown 改成 onscroll,mouseEvent 就會變成 Event 型別(沒有 button 屬性),TS 會立刻報錯。這就是上下文推論的威力。
何時該依賴推論?
黃金法則:只要 TS 猜得對,就不要寫型別註解。
這會讓你的程式碼看起更乾淨、更像現代 JavaScript,而且當你重構程式碼(例如改變初始值)時,不需要去改一大堆型別註解。
例子:不需要註解
// 不需要寫 let name: string = "小明";
let name = '小明';
// 不需要寫 const numbers: number[] = [1, 2, 3];
const numbers = [1, 2, 3];
例子:需要註解
當 TS 猜錯,或者猜得「太寬」時。
// 1. 延後賦值
let data; // TS 猜這是 any,因為你沒給值
data = 123;
data = 'hello'; // 這樣會變很危險,失去檢查功能
// 修正:明確告訴它
let safeData: string;
safeData = 'hello';
const vs let 的推論差異
這是一個很重要的細節。
let a = 'hello';
// a 的型別是 string
// 因為 a 是 let,隨時可能被改成別的字串,例如 a = "world"
const b = 'hello';
// b 的型別是 "hello" (Literal Type)
// 因為 b 是 const,永遠不可能變成別的值
這在推論物件屬性時特別重要。因為物件屬性是可以被修改的,所以 TS 預設會推論得比較「寬」。
const options = {
method: 'GET', // 推論為 string,而不是 "GET"
timeout: 5000,
};
// 假設有一個 request 函式要求 method 必須是 "GET" | "POST"
// request(options.method); // 可能會報錯,因為 string 不等於 "GET" | "POST"
解決方法是使用 as const:
const options = {
method: 'GET',
timeout: 5000,
} as const;
// 現在 method 被鎖死為 "GET" (readonly)