TypeScript 的類型註釋基礎

January 7, 2019

Photo by Vita Vilcina on Unsplash

JavaScript 是一個弱型別的語言,當你宣告一個變數時,可以不需要指定型別,它在 runtime 的時候會自動推導、轉換該變數的型別,也可以說它是動態的程式語言,這樣的特性對於一開始撰寫程式碼非常的快速方便,但是當程式碼開始龐大時,如果沒有事先規範和定義,對於參數的傳遞、函式的回傳常常會存在許多不確定性,這些都是潛在的問題,而 bug 往往都發生在這些之中,但若是使用強型別的語言,在編譯(compile)的過程中就可以先抓出類型的錯誤,確認變數是否符合該型別。

前陣子使用 Go 語言寫了一些 side project,深刻地感受到了強型別的好處,對於傳入跟回傳的參數都是可以預期的,降低了對參數型別的不確定性,感覺蠻好的。在 JavaScript 中,可以透過 FlowtypeTypeScript 來為 JavaScript 加上型別,以前有使用過幾次 Flowtype,但是因為後來版本升級爆炸後就沒再使用了 😂,而 TypeScript 則是完全沒有使用過,所以想在新的一年為自己的技能樹再點上一點 😛。

文章主要是記錄基礎簡單的使用,想要深入了解可以參考 TypeScript 文件的 Handbook

如果想要嘗試文章內的程式碼,提供一個 CodeSandbox 的連結,或是使用 TypeScript 的 Playground

Primitive Data Type

在 JavaScript 中有 7 種原始型別:

  • Number
  • String
  • Boolean
  • Null
  • Undefined
  • Symbol
  • Object(包含 FunctionArray

💡 對於原始型別的認識如果還不是很熟悉的,強烈建議閱讀 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, // 0
  Running, // 1
  Biking // 2
};

又或者你可以自定義這些 enum 的值:

enum Sport {
  Basketball = 1,
  Running = 3,
  Biking = 5
}

let s: string = Sport[3];
console.log(s) // Running

s = 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 Function
function foo(x, y) {
  return x + y;
}

// Anonymous Function
const 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.'); // ✅ Okay
greeting('Peng Jie'); // ✅ Okay
greeting('Peng Jie', undefined); // ✅ Okay
greeting('Peng Jie', null) // 🔴 Error

通常 nullundefined 不包含在型別之中,它們是單獨,沒有交集(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,這樣就可以透過這些 interfaceabstract 來操作不同的物件。

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 Jie
console.log(age); // 25

Reference