TypeScript 筆記:推斷、註記與斷言


Posted by SimonAllen on 2021-08-06

和 JavaScript 相比,TypeScript 最容易被提出差異的就是型別系統,其核心主要為註記斷言推論

型別推論 Inference

在撰寫 TypeScript 時,如果沒有指出型別,那麼編譯器會照型別推論來推測型別。

例如,這樣寫會報錯:

let years = '2021';
years = 2021;

在前面 let years = '2021'; 這段,雖然我們沒有使用斷言與註記限制變數 years 型別,但 TypeScript 按照型別推論規則推測 years 是字串型別(因為我們賦值了字串 "2021") 。
所以當後續 years = 2021; 要重新賦值數值時變報錯。

也就是等於

let years: string = '2021';
years = 2021;

非常貼心!

至於使用型別推論的時機,要先理解為什麼開發者要額外規範型別這這件事:使用註記與斷言目的是為了替開發者縮減型別,讓編譯器協助偵錯,減少開發上遇到的 bug。

如果宣告變數卻沒定義型別,也就是所謂延遲性指派的話,原生 JavaScript 變數預設型別是 undefined,在 TypeScript 卻不同,變數會被推斷成 TypeScript 的 any 型別,後續可以再賦值其他型別而不報錯,但常用 any 的話那使用有型別系統的 TypeScript 就沒意義了,建議慎用。

let years; // 型別推論 any 型別
years = 2021; // 不會報錯

以下情境,建議少用型別推論,視情況撰寫註記或斷言比較好:

  • 先宣告變數才賦值(延遲性指派)
  • 函式參數
  • 函式回傳值

因為不知道我們開發者後續邏輯還會指派、傳入或回傳什麼值,所以只用型別推論風險會增加很多。

不過雖然註記與斷言很重要,但完全補上註記與斷言會讓程式太過攏長,適當的使用型別推論,不僅能減少程式碼長度,也可以提升開發者閱讀體驗,個人認為不需要 100% 龜毛的使用註記與斷言無限上綱,若程式命名夠語意化,也可以適當的使用型別推論。

型別註記 Annotations

型別註記 Annotations 是 TypeScript 型別系統最常見的標註方式,註記: 更是開發者撰寫 TypeScript 時最、最基礎的用法。

1. 註記:

例如:

const food: string = "香雞排";

等於

const food = "香雞排";

重點是 : string 這段,通常用:冒號後面接型別方式定義。

2. 註記 =>

除了 : 外,還有箭頭 =>,這裡指的是 TypeScript 的=> 而非 ES6 => 的箭頭函式,TypeScript 的=> 通常用在函式表達式上。

例如,我們先僅用:來註記函式型別

let test = function (a: number, b: number): number {
    return a * b;
};

這段程式碼定義被指派給 test 變數的函式,該函式的參數與回傳值都是 number,而函式回傳值型別是在 er): number { 這邊的 : number 定義。

但其實, test 變數的型別,是由型別推論推測而來,我們剛剛註記的是要賦予過去的函式,而沒有註記被賦值得 test 變數型別。

要完全註記清楚,龜毛一點就是變數與賦予的內容都要註記型別,改成如下

let test: (a: number, b: number) => number = function (a: number, b: number): number {
  return a * b;
};

let test: (a: number, b: number) => number 這段 : (a: number, b: number) => number,指的是我們註記變數 test 是一個函式與回傳值,在 TypeScript 型別規則中,=> 用來表函式輸出型別,而 ES6 中,=> 是箭頭函式,所以再極端一點的寫法,將上述程式碼 function 改成箭頭函式會變這樣。

let test: (a: number, b: number) => number = (a: number, b: number): number => {
    return a * b;
}

最終編譯出來是這樣

let test = (a, b) => {
    return a * b;
};

變數與賦予的值都進行了註記,看似很嚴謹但也增加了閱讀上的困難,在撰寫上其實不用這麼極端。通常視開發者情況,let test: (a: number, b: number) => number 這段不會註記,而是讓變數透過型別推論來推測。

型別斷言 Assertiong

型別斷言只能用在 Expression 表達式上,因為 Expression 表達式會返回值,斷言會覆蓋編譯器對該值的型別推論,對該值做出型別的限制。除了開發者用斷言來規範自行撰寫的表達式外,另一個使用斷言的表達式情境,是對 3rd library 或環境物件內建、原生 Method,因為編譯器不曉得其會返回什麼型別,僅用型別推論就不是那麼恰當。

常見寫法是:

值 as 型別

例如假如我們用了一個第三方函式 getName,開發者預期呼叫它會回傳字串,這時可以這樣寫:

const name = getName() as string

注意as 型別 斷言不是寫在宣告的部分,而是寫在 Expression 表達式後。

另一種寫法是

<型別>值

我們可以把<型別>放在 Expression 表達式前,例如這樣寫

const a = <number>(1 + 1);

不過要注意:<型別>可不是 React 或 Vue 元件,若是報錯就要查看編譯器 config 設定。

提醒若是使用 TypeScript 官網的遊樂場線上編輯器,無痕瀏覽器開啟預設 config 是 React JSX 而不是 none,這導致 const a = <number>(1 + 1); 在官網遊樂場編輯器預設 run 會報錯!因為它把<型別>當成<元件>,但本機電腦 tsc 預設 run 是正常的。

註記與斷言的差異

  • 型別註記大多用在宣告階段(變數、物件與函式..等等),規範其內容型別。
  • 型別斷言只能用在 Expression 表達式上。
  • 型別斷言大多用在 3rd 方法或物件,需要開發者後續指定其(返回、接收值)型別的情境。
  • 型別斷言會覆蓋型別推論的結果告訴編譯器,開發者預期限制的型別為何。

參考

TypeScript 新手指南
TypeScript: Typed JavaScript at Any Scale.
kobo 電子書:讓TypeScript成爲你前端開發的ACE!
Typescript 初心者手札


#TypeScript









Related Posts

How does Event Loop work?

How does Event Loop work?

[Note] JS: Function 觀念

[Note] JS: Function 觀念

Day 176

Day 176


Comments