2024-02-20|閱讀時間 ‧ 約 32 分鐘

【前端開發】TypeScript 的型別推論、型別註記與型別斷言

上一篇文章分享了 TypeScript 的定義、前端角色定位,如果你不是很確定「TypeScript 是什麼?」、「TypeScript 作為 JavaScript 的超集,在網頁開發扮演怎麼樣的角色?」這兩個問題的答案,建議可以回到上一篇先了解一下。

這一篇主要會來聊聊 TypeScript 初學者首先會遇到的幾個基礎概念,分別是:型別推論(Type Inference)、型別註記(Type Annotation)與型別斷言(Type Assertion)。

這幾個概念非常重要,如果想要深入了解 TypeScript 人一定會在 TypeScript 的官方文件或是 Stack overflow 上不斷遇到,建議在初學時就先大致了解。

在理解這幾個概念之前,建議大家要對 JavaScript 的型別系統、自動轉型的特性有基礎了解,並且了解自動轉型及缺乏型別定義的工具,會對開發上帶來什麼樣的困擾,如此才能理解今天要討論的主題可以解決什麼樣的問題。

那麼就讓我們接續之前的討論,先來聊聊 TypeScript 的型別註記系統吧:

TypeScript 的型別註記

型別註記在 TypeScript 中最基礎的概念,它可以協助開發者針對變數、參數、回傳值進行型別的標註,當註記的變數、參數及回傳值被帶入型別有誤的資料時,TypeScript 就會給予報錯。

我們來看看最基本的型別註記在 TypeScript 中是怎麼運作的,首先是最簡單的變數型別標註:

// 標註變數為字串型別
const name: string = 'Vivian';

我們會使用一個半形冒號,加上 JavaScript 中的型別來進行註記,上方的範例是將一個名為 name 的變數標註為字串型別,如果我們在標註好型別的變數戴上錯誤的型別的話,TypeScript 就會報錯:

// Type 'string' is not assignable to type 'number'.
const name: number = 'Vivian';

在使用靜態資料的狀況下我們其實很少會標註型別,這個我們後續會說明原因,接著來看看我們如何針對函式標註型別:

// 具名函式陳述式
function getString (str: string) : string {
return str
};

// 箭頭函式
const getString = (str: string): string => str;

在具名函式陳述式(Statement)中,我們可以在參數後面一樣加上冒號,在冒號後接續你想要標注的參數型別,針對回傳值的型別則接續在參數後加上冒號,接著標註你想要回傳的值型別。

不過通常我們滿少看到函式陳述式或是函式表達式(Expression)的,一般都是看到箭頭函式(Arrow Function)比較多,因為寫起來快速且看起來較為簡潔。 初學者可能會因為箭頭函式比較簡潔所以比較難理解標註的方式,不過這其實只要夠瞭解箭頭函式的運作方式,多看幾次就會習慣了:

// JavaScript 箭頭函式
const getString = str => str;

// TypeScript 箭頭函式
const getString = (str: string): string => str;

其實標註的方式跟一般的函式差不多,在參數後面一樣加上冒號,在冒號後接續你想要標注的參數型別,針對回傳值的型別則接續在參數後加上冒號,接著標註你想要回傳的值型別。

這樣我們就了解 TypeScript 中最基礎的型別標記啦,接著我們來看看 TypeScript 另外一個基礎、同時非常重要的概念:型別推論。

TypeScript 的型別推論

前文我們有提到,在使用靜態資料的狀況下我們其實很少會標註型別,這是為什麼呢?

由於 TypeScript 是透過靜態型別系統,在 JavaScript 編譯時透過上下文來「推斷」程式碼與型別標注的關係,例如:明明就標註回傳值要回傳字串型別,但卻回傳了數字,那麼 TypeScript 就會報錯。

所以其實在使用靜態資料的狀況下,我們其實就算不特別標註 TypeScript 也可以推論出指定變數的型別,以下範例在我們把滑鼠移到變數上時,TypeScript 會跳出它推論的出對應型別:

// let personName: string
let personName = '';

假設我指給這個變數錯誤的型別的話,TypeScript 一樣會報錯:

let personName = '';
// Type 'number' is not assignable to type 'string'.
personName = 123;

因此提供預設值在 TypeScript 中就會顯得特別重要,假設我們針對這個靜態資料沒有提供預設值,註解的地方是 TypeScript 顯示的型別提示:

// let personName: any
let personName;

// let personName: any
personName = 123;

// let personName: any
personName = '123';

會發現 TypeScript 會將沒有標註的型別變數推論成 any ,any 型別在 TypeScript 的意義是「可以接受任何型別的資料」,於是乎就讓我們的程式碼從 TypeScript 變回弱型別的 JavaScript 了,完全失去我們使用這個程式語言的意義。

雖然 TypeScript 擁有自動推論型別的功能,但最好只使用在「靜態、有預設值」的變數上,不然 TypeScript 可能會將回傳值的型別推論為 any,或是出現一些預期外的型別。

問題來了?對於函式的參數與回傳值如果都不標註型別,TypeScript 可以幫我們推論嗎?答案是:否,因為 TypeScript 無法針對在沒有前後文、沒有型別標註的未知參數進行推論,因此型別就會變成 any。

因此,也有開發者認為有限度的標註可以使 TypeScript 的程式碼更加簡潔且好維護,例如:針對函式嚴格標註、限制可被使用的型別,但靜態資料、預設值則讓 TypeScript 自動推論,我個人是很認同這種做法,給大家做參考。

TypeScript 的型別斷言

在 TypeScript 有一種酷東西叫做型別斷言(Type Assertion),這個算是自己在初學 TypeScript 時 最搞不懂的東西,在被自己一些沒寫好的 TypeScript 荼毒後,才理解所謂的斷言是怎麼一回事。

雖然可以把型別定義好是最理想的狀況,但有些狀況底下,還真的會有「未知(unknwon)型別」的出現,斷言就是用來處理未知型別的狀況。

關於 unknown 型別,大家可能最普遍會搞混的是:unknown 與 any 的差異是什麼?這個觀念也很重要,建議大家在初學 TypeScript 就可以養成好習慣:

unknown 是未知,any 是任意,未知並不代表這個變數想要接受「隨便一種」型別。

講了那麼多,讓我們來看看 TypeScript 官網針對斷言的範例:

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

範例中用了 getElementById 這個方法去取得指定的 DOM 元素,不過 DOM 元素在 TypeScript 中也有屬於其對應的型別,舉例來說(此處僅列舉常用的型別):

  • HTMLElement:代表一般的 HTML 元素,如 <div><span> 等。它是最基本的 DOM 元素型別,包含了一些基本的屬性和方法。
  • HTMLInputElement:代表 <input> 元素,用於處理使用者輸入的表單元素。它擁有 HTMLInputElement 特有的屬性和方法,如 valuechecked 等。
  • HTMLSelectElement:代表 <select> 元素,用於選擇列表。它提供了相關的屬性和方法,例如 selectedIndexoptions 等。
  • HTMLTextAreaElement:代表 <textarea> 元素,用於多行文本輸入。它有自己特定的屬性和方法,如 valuerows 等。
  • HTMLButtonElement:代表 <button> 元素,用於創建按鈕。它提供了與按鈕相關的屬性和方法,如 disabledclick 等。

我們要怎麼知道透過 getElementById 這個方法去取得指定的 DOM 元素是哪一種?

面對未知的型別我們就可以在 TypeScript 中,在回傳值的後方使用 as 關鍵字,並接續你想要指定的型別,當然這裡不可以出現型別的衝突,舉例來說:

/* Conversion of type 'string' to type 'number' may be a mistake 
because neither type sufficiently overlaps with the other.
If this was intentional, convert the expression to 'unknown' first. */

const x = "hello" as number;

上方的範例中,明明是字串型別,我們卻把它斷言為數字型別,此時就會報錯,TypeScript 甚至會叫你把字串型別先轉換成 unknown 型別再進行斷言。

不過這樣的使用方式顯然不合理,要極力避免,且注意實際的回傳值要是否與斷言相吻合,所以大部分會使用斷言可能是在兩種狀況:在型別沒有被推斷且可為任意型別時:

const variable = <any> as <T>;

型別未知:

const variable = <unknown> as <T>;

由於 TypeScript 型別眾多,不免會出現像是選擇 DOM 元素時,出現未知型別的問題,除非必要,不然建議不要為了符合編譯器的提示而大量使用斷言,建議在一開始就針對回傳值好好地標註型別。

除了使用 as 關鍵字,我們也可以使用下方這種方式去斷言型別未知的變數(函式回傳值),使用大於小於的符號包住你想要斷言的型別:

const element = <T>document.getElementById("element");

實際使用起來可能會長這樣:

const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");

這種用大於小於的方式相對少見,因為會跟 TypeScript 中的另外一個概念「泛型」很像,希望之後有機會可以介紹到,以上就是對於型別斷言的基礎介紹啦。

小結

這篇文章主要介紹了三個 TypeScript 中很重要的觀念,分別為:型別註記、型別推論與型別斷言,雖然了解這些觀念並沒有辦法讓你完整掌握 TypeScript 這門語言,但卻能讓我們在研讀相關程式碼、文件時更加順遂。

希望大家可以透過這篇文章,更加瞭解 TypeScript 的基礎概念,我是 Vivian,我們下次見。

參考資料:

  1. TypeScript Docs - Type Inference
  2. TypeScript Docs - Type Annotation
  3. TypeScript Docs - Type Assertion
分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.