TypeScript 的類型註釋基礎
JavaScript 是一個弱型別的語言,當你宣告一個變數時,可以不需要指定型別,它在 runtime 的時候會自動推導、轉換該變數的型別,也可以說它是動態的程式語言,這樣的特性對於一開始撰寫程式碼非常的快速方便,但是當程式碼開始龐大時,如果沒有事先規範和定義,對於參數的傳遞、函式的回傳常常會存在許多不確定性,這些都是潛在的問題,而 bug 往往都發生在這些之中,但若是使用強型別的語言,在編譯(compile)的過程中就可以先抓出類型的錯誤,確認變數是否符合該型別。
前陣子使用 Go 語言寫了一些 side project,深刻地感受到了強型別的好處,對於傳入跟回傳的參數都是可以預期的,降低了對參數型別的不確定性,感覺蠻好的。在 JavaScript 中,可以透過 Flowtype 和 TypeScript 來為 JavaScript 加上型別,以前有使用過幾次 Flowtype,但是因為後來版本升級爆炸後就沒再使用了 😂,而 TypeScript 則是完全沒有使用過,所以想在新的一年為自己的技能樹再點上一點 😛。
文章主要是記錄基礎簡單的使用,想要深入了解可以參考 TypeScript 文件的 Handbook。
如果想要嘗試文章內的程式碼,提供一個 CodeSandbox 的連結,或是使用 TypeScript 的 Playground。
Primitive Data Type
在 JavaScript 中有 7 種原始型別:
Number
String
Boolean
Null
Undefined
Symbol
Object
(包含Function
和Array
)
💡 對於原始型別的認識如果還不是很熟悉的,強烈建議閱讀 Kuro Hsu 大大的鐵人賽的「重新認識 JavaScript: Day 03 變數與資料型別」這篇文章。
Type Annotations
對於一個變數的型別註釋(Type Annotations),只要在變數後面加上 :
就可以描述該變數可以具有的值:
let gretting: string
TypeScript 也可以自動幫你推導類型,例如以上面範例來說:
let greeting = 'Hello!';
TypeScript Basic Type
除了 JavaScript 的 7 種原始型別外,TypeScript 還多了以下幾個基礎型別:
Array
Any
Enum
Void
Never
前面的 7 個型別就不再闡述了,這邊會來簡單說明上述這幾個型別。
Array
在 TypeScript 中,Array
又可以分成兩種角色(或者是兩者混合):
- Tuple:所有元素型別不一定相同,但是陣列長度固定。
- List:所有元素有相同的型別,陣列長度不同。
在 TypeScript 中,Array
的表達方式有兩種:
let array: number[] = [];let anotherArray: Array<number> = [];
上述的表達式是表示 Array
為一個 List,它沒有長度的限制。
當 Array
作為 Tuple 時,它的表達式:
let people: [string, number] = ['Peng Jie', 99];
上述的表達是可以看到,陣列的長度為 2,而且有說明元素分別應該是什麼型別。
另一個比較常見的範例是 Object.entries(obj)
,它會回傳一個帶有 [key, value]
的陣列:
const obj = { foo: 'bar', baz: 42 };console.log(Object.entries(obj)); // [['foo', 'bar'], ['baz', 42]]
而 Object.entries(obj)
的型別則會是:Array<[string, any]>
。
Any
any
顧名思就是任意的型別,它讓變數的型別可以是彈性的,有時候我們在使用第三方的函式庫時,可能沒辦法預期回傳值是什麼樣的型別,又或者在開發階段時,還無法確定該變數的確切型別是什麼的時候,這時候就可以使用 any
的型別:
const someMethod = require('some-module');let result: any = someMethod(/* parameters */);
如果陣列內的元素型別可能都不相同時:
let list: any[] = [1, true, 'hello'];
Enum
enum
內的元素起始值為 0
,接著為 1
:
enum Sport {Basketball, // 0Running, // 1Biking // 2};
又或者你可以自定義這些 enum
的值:
enum Sport {Basketball = 1,Running = 3,Biking = 5}let s: string = Sport[3];console.log(s) // Runnings = Sport[2];console.log(s) // undefined
enum
對於一些常數的處理非常的有用。
Void
void
是函式的結果型別,它總是回傳 undefined
:
function foo(): void {// ✅ Okay}function fooBar(): void {return undefined; // ✅ Okay}function fooBaz(): void {return 'fooBaz'; // 🔴 Error}
Never
never
型別代表「永遠不會有值的型別」,例如像是拋出例外的函式:
function error(message: string): never {throw new Error(message);}
Function Types
在 JavaScript 中,可以透過以下的方式來建立函式:
- 函式宣吿(Function Declaration)
- 函式運算式(Function Expressions)
new Function
這裡只討論以下這兩種函式
// Named Functionfunction foo(x, y) {return x + y;}// Anonymous Functionconst fooBar = function(x, y) {return x + y;};
在 TypeScript 中要定義這些函式也非常的容易:
function foo(x: number, y: number): number {return x + y;}const fooBar = function(x: number, y: number): number {return x + y;};
如果想要使用箭頭函式(Arrow Function)也是可以的:
- const fooBar = function(x: number, y: number): number { return x + y; };+ const fooBar = (x: number, y: number): number => x + y;
Union Types
在 JavaScript 中,變數有時候的型別可能不只一個,為了描述這種變數,可以使用 Union Types,透過 |
符號來區隔型別,例如下面的範例 point
可能是 number
或者是 null
的型別:
let point: number | null;
Optional Parameters
在 TypeScript 中,如果要定義一個參數是「可選的」,可以在該變數後面加上 ?
:
function greeting(name: string, nickName?: string): string {if (nickName) {return `Hello, my name is ${name} and my nick name is ${nickName}.`;}return `Hello, my name is ${name}.`;}
如果在 tsconfig 中設定了 --strictNullChecks
,可選參數會自動地加上 | undefined
,以上面的範例來說:
greeting('Peng Jie', 'P.J.'); // ✅ Okaygreeting('Peng Jie'); // ✅ Okaygreeting('Peng Jie', undefined); // ✅ Okaygreeting('Peng Jie', null) // 🔴 Error
通常 null
和 undefined
不包含在型別之中,它們是單獨,沒有交集(Union)的型別。若要修正上面的錯誤,可以利用上述提到的 Union Types,將 nickName
的型別修改為:
- function greeting(name: string, nickName?: string): string+ function greeting(name: string, nickName?: string | null): string
Parameter Default Values
TypeScript 支援 ECMAScript 6 函式的預設參數:
function greeting(name = 'John Doe'): string {return `Hello, my name is ${name}.`;}
參數的型別是可以省略的,TypeScript 可以自動地幫你推導型別,如上面範例 name
的型別會自動被推導成 string
,如果你想要加入型別註釋,可以修改為:
- function greeting(name = 'John Doe'): string+ function greeting(name: string = 'John Doe'): string
Rest Parameters
ECMAScript 6 的剩餘參數(Rest Parameter)在 TypeScript 中對應的型別必須是 Array
:
function joinNumbers(...nums: number[]): string {return nums.join('-');}
Interface
Type-Checking 是 TypeScript 核心之一,它主要聚焦在值的形狀(Shape)應該是什麼樣子,有時候稱為「Duck Typing(鴨子型別)」或者是「Structural Subtyping(結構型別)」。在 TypeScript 中,interface
扮演了定義這些型別的角色,它可以明確約束你的程式碼。
例如定義一個座標的點:
interface Point {x: number;y: number;}
接著建立一個函式,它接受一個 Point
屬性的參數:
function printPoint(p: Point): string {return `The x is ${p.x}, y is ${p.y}.`;}printPoint({ x: 1, y: 2 }) // The x is 1, y is 2.
這時候傳入的參數如果不符合 Point
,就會造成編譯錯誤。
若屬性是可選的,可以在屬性後面加上 ?
符號,例如:
interface Point {x: number;y: number;+ z?: number;}
Generics
以前在學物件導向時,應該多少有聽過多型(Polymorphism) (雖然自己很少用到 😅),簡單來說多型是透過將不同的物件,抽象成 interface
或者是 abstract
,這樣就可以透過這些 interface
或 abstract
來操作不同的物件。
Generics,中文翻譯稱作為泛型,它的概念有點類似於多型,但它是透過參數型別(Type Parameter)將不同的物件的型別給抽象化。
我覺得中文翻譯成「泛型」非常的有趣,Generic
在英文可以解釋為通用的的意思,Generics 泛型就是指「泛指所有的類型」,接下來讓我們看一個簡單的範例。
如果有一個函式,當輸入了參數它就會回傳那個參數,例如傳入的參數型別為 number
:
function identity(arg: number): number {return arg;}
如果我們希望這個函式可以接受不同種類的參數時,勢必會遇到一些麻煩,這時候你可能會想要使用 any
的型別:
function identity(arg: any): any {return arg;}
但是使用 any
型別會讓我們沒有辦法清楚得知傳入的參數和回傳的參數的具體型別,這樣並沒有解決問題,但是可以藉由 Generics(泛型),透過類型參數(type parameter)傳入型別,就可以將各個參數或是物件的型別給抽象化。
function identity<T>(arg: T): T {return arg;}
透過 <>
符號將型別傳入,這時候可以 identity
函式就可以接受任意的 T
型別,若沒有傳入 T
型別,TypeScript 會自動推導型別:
const userName = identity<string>('Peng Jie');const age = identity<number>(25);console.log(userName); // Peng Jieconsole.log(age); // 25