Skip to main content

Advanced TypeScript

型別別名

函式的出現是為了把常用的邏輯包在一起以簡化程式碼增加維護與可讀性。型別的別名也是相同的道理,將常用的型別組合宣告成別名來簡化程式碼。例如:

未用型別別名前
const addNum = (x: number, y: number): number =>{
return x + y
}

const minusNum = (x: number, y: number): number =>{
return x - y
}

兩個函式都需要為參數和回傳值指定同樣的型別,那這時就可以把相同的內容抽出來透過 type 來定義別名,並把這個別名作為型別給註記在函式下:

type NumType = (x: number, y: number)=> number

const addNum: NumType = (x, y) =>{
return x + y
}

const minusNum: NumType = (x, y) =>{
return x - y
}

字串字面量型別 (String Literal)

也是使用 type 做定義,所以請把它跟型別別名一起看。

字串字面量型別名字很長,它可以讓我們自己定義一組字串值,而這組字串值可以做為型別註記在變數底下。

像下面例子,當 heading 變數型別註記為 Direction 這個字串字面量型別,那它內部所帶的值就必須滿足 Direction 定義的內容:

type Direction = "north" | "south" | "east" | "west";

let heading: Direction;

heading = "north"; // 正確
heading = "east"; // 正確
heading = "up"; // 錯誤,因為 "up" 不是合法的值
tip

字串字面量型別對以下情況特別有用:

  1. 想限制變數、函式參數或物件屬性只能接受特定字串值時。
  2. 因為可以預期可能的值是哪些,所以提高了程式碼可讀性。

元組 (tuple)

他長得很像陣列,但比陣列嚴苛許多。元組一旦定義下去,它每個 index 該放什麼型別資料以及有多少 index 都必須完全符合定義。舉例來說:

let arr: [string, number, string]

arr = ['hello', 100, 'world']

一旦定義了元組,後面賦值時就該滿足元組內定義的型別以及數量。
所以說元組在需要保持特定順序和型別的情境下非常有用。比方說,拿來存每個商品的的名稱、價格、購買數量,用這樣一組元組為基礎建立一個購物車函式內容:

// 建立一個元組用來存商品名稱、數量以及價格
type CartItem = [string, number, number]

// 創建購物車
function createShoppingCart(): {
// 回傳內容型別宣告
items: CartItem[],
addItem: (item: CartItem) => void,
calculateTotal: () => number
} {
const items: CartItem[] = [];

// 添加購物車項目
function addItem(item: CartItem): void {
items.push(item);
}

// 計算總價
function calculateTotal(): number {
return items.reduce((total, [_, price, quantity]) => total + price * quantity, 0);
}

return { items, addItem, calculateTotal };
}

// 使用購物車
const shoppingCart = createShoppingCart();

// 添加一些項目到購物車
shoppingCart.addItem(["Laptop", 1000, 2]);
shoppingCart.addItem(["Mouse", 20, 3]);
shoppingCart.addItem(["Keyboard", 50, 1]);

// 計算並輸出總價
const totalPrice = shoppingCart.calculateTotal();
console.log(`Total Price: $${totalPrice}`);

列舉 (enum)

為抽象的數字賦予有意義的名字,來提高程式碼的可讀性。

舉例來說,要表示星期幾,可能會用數字,比如 1 代表星期一,2 代表星期二,以此類推。但是單純用數字寫對於閱讀程式碼的人來說可能不太直觀。

enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat}

從 Sun 開始,這些名字會依序被賦值 0-7,換句話說,我們正在用有意義的名字來表示一組固定的值。現在,就可以使用 Day.Monday 來表示星期一,而不是使用不直觀的數字。

泛型 (Generics)

泛型說白話一點有些把型別當變數用,稍微形象一點形容可以把它想像成一個靈活的容器,允許我們使用不同的型別,而不失去型別安全性。

比如有一個函式,希望傳入參數是 string 時回傳也是 string,就可以利用泛型:

function myFunc<T>(value: T): T {
return value;
}

let result = myFunc("Hello, TypeScript!");
console.log(result); // "Hello, TypeScript!"

上面發生了什麼事呢?

  1. "Hello, TypeScript!" 作為參數傳入函式。
  2. "Hello, TypeScript!" 型別推論為 stringT 因此表示型別為 string
info

不過其實談到泛型,就會不由自主想到聯合型別過載,同樣都是為了處理多型別代入的情形。實際上在一些情況下,三者達成的功能是一樣的,差別在於語法。至於具體差異大致上是:

  1. 泛型相對靈活與通用。
  2. 聯合型別+型別斷言通常用於處理明確的型別轉換。
  3. 過載則用於提供不同輸入的特定處理邏輯。

info

補充 tsconfig.json

前面已經先全局安裝過 TypeScript 了,並且新增了 hello.ts 檔案來練習 TS 語法並透過 tsc hello.js 來編譯 TS 檔案。

但在實務上要開始寫 TS 前會先在一個 tsconfig.json 的檔案中做編譯設定,前面只是為了快速練習 TS 語法所以先跳過這部分。

tsc --init

執行完畢,tsconfig.json 會出現在專案資料中。

打開設定檔,先把那些林林總總的註解拿掉,會看到 TS 的預設是這樣:

tsconfig.json
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
  1. target:指定編譯生成的 JavaScript 的版本。
  2. module:指定了生成模組的格式。CommonJS 是 Node.js 中常見的模組系統。
  3. esModuleInterop:允許 TypeScript 對 CommonJS 模組進行更好的交互操作。
  4. forceConsistentCasingInFileNames:TypeScript 將嚴格檢查文件名的大小寫一致性。
  5. strict:啟用所有的嚴格類型檢查選項
  6. skipLibCheck:跳過對宣告文件 (第三方套件) 的檢查。

基本上只用預設設定 TS 也能動,但可以客製化讓 TS 更方便,而那一大包註解就是 TS 允許自己調整的設定內容。

最後說一下自動編譯。當我在學 TS 每次在那邊 tsc hello.ts 時都在想,SCSS 有自動檢測編譯的功能,TS 有沒有?
還真的有,在終端機輸入 tsc --watch 就可以讓 TS 每次在儲存檔案時就自動編譯。而像我一樣 VScode 設定每次變動都自動儲存的人,這就等同無時無刻都在自動編譯了。

參考資料

  1. TypeScript 新手指南
  2. 【Day 12】TypeScript 資料型別 - 元組(Tuple) & 列舉(Enum)
  3. 第二週第一天:型別別名與字串字面量型別
  4. TypeScript | 善用 Enum 提高程式的可讀性 - 基本用法 feat. JavaScript