上一篇文章分享了 TypeScript 的定義、前端角色定位,如果你不是很確定「TypeScript 是什麼?」、「TypeScript 作為 JavaScript 的超集,在網頁開發扮演怎麼樣的角色?」這兩個問題的答案,建議可以回到上一篇先了解一下。
這一篇主要會來聊聊 TypeScript 初學者首先會遇到的幾個基礎概念,分別是:型別推論(Type Inference)、型別註記(Type Annotation)與型別斷言(Type Assertion)。
這幾個概念非常重要,如果想要深入了解 TypeScript 人一定會在 TypeScript 的官方文件或是 Stack overflow 上不斷遇到,建議在初學時就先大致了解。
在理解這幾個概念之前,建議大家要對 JavaScript 的型別系統、自動轉型的特性有基礎了解,並且了解自動轉型及缺乏型別定義的工具,會對開發上帶來什麼樣的困擾,如此才能理解今天要討論的主題可以解決什麼樣的問題。
那麼就讓我們接續之前的討論,先來聊聊 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 是透過靜態型別系統,在 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 有一種酷東西叫做型別斷言(Type Assertion),這個算是自己在初學 TypeScript 時 最搞不懂的東西,在被自己一些沒寫好的 TypeScript 荼毒後,才理解所謂的斷言是怎麼一回事。
雖然可以把型別定義好是最理想的狀況,但有些狀況底下,還真的會有「未知(unknwon)型別」的出現,斷言就是用來處理未知型別的狀況。
關於 unknown 型別,大家可能最普遍會搞混的是:unknown 與 any 的差異是什麼?這個觀念也很重要,建議大家在初學 TypeScript 就可以養成好習慣:
unknown 是未知,any 是任意,未知並不代表這個變數想要接受「隨便一種」型別。
講了那麼多,讓我們來看看 TypeScript 官網針對斷言的範例:
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
範例中用了 getElementById 這個方法去取得指定的 DOM 元素,不過 DOM 元素在 TypeScript 中也有屬於其對應的型別,舉例來說(此處僅列舉常用的型別):
<div>
、<span>
等。它是最基本的 DOM 元素型別,包含了一些基本的屬性和方法。<input>
元素,用於處理使用者輸入的表單元素。它擁有 HTMLInputElement 特有的屬性和方法,如 value
、checked
等。<select>
元素,用於選擇列表。它提供了相關的屬性和方法,例如 selectedIndex
、options
等。<textarea>
元素,用於多行文本輸入。它有自己特定的屬性和方法,如 value
、rows
等。<button>
元素,用於創建按鈕。它提供了與按鈕相關的屬性和方法,如 disabled
、click
等。我們要怎麼知道透過 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,我們下次見。
參考資料: