在此要討論的是有關 TypeScript 的 型別推論(type inference) 與 型別註記(type annotation)
關於 Javascript 的型別
Javascript 是個動態型別的語言,也就是說擁有 ‘型別’ 的是 ‘值’ 而不是變數。
let myVar;
console.log(`${myVar}: ${typeof myVar}`); // undefined : undefined
myVar = 20;
console.log(`${myVar}: ${typeof myVar}`); // 20 : number
myVar = "Hi";
console.log(`${myVar}: ${typeof myVar}`); // Hi : string
myVar = true;
console.log(`${myVar}: ${typeof myVar}`); // true : boolean
Type Annotation (型別註記)
TypeScript 提供靜型別功能,讓我們可以明確指定變數的型別。編譯器在轉譯過程即可偵測到使用不同型別時主動抛出錯誤訊息。下面的例子就是使用型別註記來定義靜態型別:
function calculateTax(amount: number): number {
return amount * 1.2;
}
console.log(`${20}: ${calculateTax(20)}`); //20: 24
console.log(`${"Hello"}: ${calculateTax("Hello")}`); //error
console.log(`${true}: ${calculateTax(true)}`); //error
Type inference (型別推論)
TypeScript 編譯器可以根據變數宣告時給定的值來推論它的型別。
function calculateTax(amount: number): number {
return amount * 1.2;
}
let price = 100; //未給定資料型別,由給定的值推論此時的 price 變數型別為 number
let taxAmount = calculateTax(price); //未給定資料型別,由回傳值推論此時的變數型別為 number,因為 calculateTax 型別註記為 number
let halfShare = taxAmount / 2; //未給定資料型別,由計算結果值推論此時的變數型別為 number
console.log(`Full amount in tax: ${taxAmount}`); //Full amount in tax: 120
console.log(`Half share: ${halfShare}`); //Half share: 60
調整 tsc 編輯參數來查看編譯器所採用的型別
tsc 編譯器推論的型別若與預期的有出入,可以在 tscconfig.json “compilerOptions"中加入一條"declaration”: true 的設定值
{
"compilerOptions": {
"target": "es2020",
"outDir": "./dist",
"rootDir": "./src",
"declaration": true
}
}
上述範例改成:
function calculateTax(amount: number) {
return (amount * 1.2).toFixed(2);
}
let price = 100; //未給定資料型別,由給定的值推論此時的 price 變數型別為 number
let taxAmount = calculateTax(price); //未給定資料型別,由回傳值推論此時的變數型別為 string,因為 calculateTax function toFixed(2) 結果是 string
let halfShare = taxAmount / 2; // 產生錯誤
console.log(`Full amount in tax: ${taxAmount}`); //Full amount in tax: 120
console.log(`Half share: ${halfShare}`); //Half share: 60
由於 toFixed(2) 回傳的是 string,導致 taxAmount 變數型別被推論成 string 而產生錯誤警告。
tsc 因為在 tscconfig.json 多加入了編輯參數 “declaration”: true,這個參數告訴編譯器,除了輸出程式碼轉譯外,還要輸芔包含型別宣告資訊的 .d.ts 檔,所以會在 build 目錄中產生一個 index.d.js,內容如下圖,可以看到 taxAmount 被推論成 string
declare function calculateTax(amount: number): string;
declare let price: number;
declare let taxAmount: string;
declare let halfShare: number;
關於 Any 的型別推論
將範例改成如下圖,calculateTax 回傳值及傳入參數皆註記成為 any
function calculateTax(amount: any): any {
return (amount * 1.2).toFixed(2);
}
let price = 100; //未給定資料型別,由給定的值推論此時的 price 變數型別為 number
let taxAmount = calculateTax(price); //由於 calcaulateTax 型別註記為 any,故被推論成 any
let halfShare = taxAmount / 2; // 未給定資料型別,(any / 2) 計算結果會被推論成 number
console.log(`Full amount in tax: ${taxAmount}`); //Full amount in tax: 120.00
console.log(`Half share: ${halfShare}`); //Half share: 60
結果發現在 index.d.js 檔中 taxAmount 也被推論成 any,且編譯結果正常。
declare function calculateTax(amount: any): any;
declare let price: number;
declare let taxAmount: any;
declare let halfShare: number;
但這樣的使用 any 型別,你會發現這和寫 javascript 程式碼無任何差別,也就是說你讓程式自行承擔結東,而沒有用到 TypeScript 靜態型別的好處(編譯時期即可檢查出程式可能出問題的地方)。
我們來測試一下,將 calculateTax function 改成如下:
function calculateTax(amount: any): any {
return `$${(amount * 1.2).toFixed(2)}`;
}
再執行看看,發現編譯程式時正常,確在執行時期產生非預期結果
Full amount in tax: $120.00
Half share: NaN
結論就是你應該儘可能的不要有 any 這樣的型別註記。
Contextual Typing (依照背景來進行的型態推論)
TypeScript 使用變量的位置來推斷它們的類型,這就是所謂的 contextual typing
document.addEventListener("click", function (event) {
console.log(event.button); //
});
在這個例子中,TypeScript 因為 click 事件而知道 event 參數是 MouseEvent 的一個實例。
document.addEventListener("scroll", function (event) {
console.log(event.button); // compiler error
});
而在這個例子中,因為是 ‘scroll’ 事件,所以參數中不能是 button,因此轉譯時會產生錯誤。
window.onmousedown = function(mouseEvent) {
console.log(mouseEvent.button); //<- OK
console.log(mouseEvent.kangaroo); //<- Error!
};
Typescript 型別檢查器使用 Window.onmousedown 函數的型別來推斷賦值右邊的函數運算式的型別。因此,它能夠推斷出 mouseEvent 參數的型別,它確實包含一個按鈕屬性,但不包含袋鼠屬性。