Skip to main content

Basic TypeScript

TypeScript 起手式

  1. 安裝 TypeScript 編譯器。
npm install -g typescript
  1. 建立一個 ts 檔案,如:hello.ts。
  2. 輸入:
function greet(person: string){
console.log(`Hello, ${person}`)
}

greet('world')
  1. 打開終端機輸入:tsc hello.ts
  2. 執行編譯過後出現 JavaScript 檔案。

型別註記

TypeScript 的出現就是為了解決 JavaScript 型別組織鬆散的問題。所以 TypeScript 的核心就是讓我們手動為我們的變數們加上型別,然後由 TypeScript 為我們做型別的把關,避免因為型別而出現一些非預期的錯誤。

比方說下面這個很經典的算術例子:

JavaScript
const sum = (x, y) =>{
return x + y
}

console.log(sum(1, '1'))

預期只是數字相加,但在程式碼某處突然傳入一個字串,就會產生這種 1 + '1' = 11 的錯誤了,這是 JavaScript 型別轉換的鍋。

改成 TypeScript:

const sum = (x: number, y: number): number =>{
return x + y
}

console.log(sum(1, '1'))

在編譯時 TypeScript 就會跳出來說:Argument of type 'string' is not assignable to parameter of type 'number'.

上述的例子就是為參數和函式的 return 做了型別註記,TypeScript 就會依照註記去做檢查。

對於 JavaScript 的基礎型別:booleannumberstringnullundefinedsymbol,通通可以用像下面這個例子這麼簡單的方式做型別註記:

let isDone: boolean = false
danger

TypeScript 提供一個型別叫做 any,代表什麼型別都可以,請盡量不要使用,不然就喪失 TypeScript 的使用意義了。

型別推論

let myName = 'Jeremy'

如果在第一次宣告變數時有給予值,其實 TypeScript 就隱性地為這個變數加上一個型別了。

聯合型別

聯合型別就是讓我們的變數可以儲存多種型別中的一種:

let num: number | string 
num = 9
num = 'nine'
warning

TypeScript 只能存取聯合型別中各型別共有的方法。

const getLength = (x: string | number):number =>{
return x.length
}

TypeScript 丟了一個錯誤:Property 'length' does not exist on type 'string | number'.
那是因為 length 不是 number prototype 下的方法,但如果是像 toString 這種兩者都有的方法就可以編譯成功

型別斷言

手動指定一個值的型別,白話一點就是為參數指定在什麼型別下要幹什麼事。以用在前面的聯合型別為例。

wrong answer
const getLength = (x: string | number):number =>{
if(x.length){
return x.length
}else{
return x.toString().length
}
}

依照上面聯合型別知道的,這東西一定會噴錯,但如果加上型別斷言就不一樣:

right answer
const getLength = (x: string | number):number =>{
if((x as string).length){
return (x as string).length
}else{
return x.toString().length
}
}

物件的型別註記

跟基本型別的註記不一樣,物件的型別註記會透過先建立介面 (interfaces) 這個模板來做型別的註記。

基本用法

  1. 先建個叫 Item 的介面:
interface
interface Item {
name: string,
price: number
}
  1. 把這個介面作為型別註記在新的物件下:
let phone: Item = {
name: 'IPhone',
price: 34000
}

這樣做的限制就是物件變數必須長得跟介面一致,不能少放一個屬性,也不能多放一個屬性。

可選屬性

用來解決不能少放屬性的問題。比如今天某個屬性可有可無,比如價格,可以在介面以可選屬性來為它做定義:

interface Item {
name: string,
price?: number
}

任意屬性

用來解決不能多放屬性的問題。

interface
interface Item {
name: string,
price?: number,
// 添加一個任意屬性
[propName: string]: any
}

這樣就能新增一個原先不存在的屬性了:

let phone: Item = {
name: 'IPhone',
date: '2023-11-13'
}

唯讀屬性

在屬性不想被更動時,可以在前面加上 readonly 讓它變成唯讀:

interface Item {
readonly name: string,
price?: number,
[propName: string]: any
}

陣列的型別

陣列的型別只要使用 型別 + [] 就可以了:

const arr: number[] = [1, 2, 3]
info

介面也可以來做陣列的型別註記,但比較麻煩

函式的型別

典型例子
const sum = (x: number, y: number): number =>{
return x + y
}

可選參數

上述這樣寫就必須嚴格遵守 TypeScript 中的規範,定義多少參數就該給多少參數,一個不多、一個不少,但同樣我們也可以把參數定義為可選參數來決定是否可以不要傳入這個參數:

const sum = (x: number, y?: number):number =>{
return (x + y)
}
warning

這裡定義 y 為可選參數而不是 x 是有小規範的,那是因為可選參數後面不允許出現必須參數了,所以請記得可選參數永遠要寫在後面。

也可以為參數代入預設值:

const sum = (x: number, y: number = 1):number =>{
return (x + y)
}
info

這時 y 會被自動當作是可選參數,但它不受 可選參數後面不允許出現必須參數 這條規則規範了。

剩餘參數

function concatString(separator: string, ...strings: string[]): string {
return strings.join(separator);
}

const result = concatString(", ", "apple", "banana", "orange")
console.log(result) // apple, banana, orange

... 表示接收數量不定的參數,並存入到一組陣列中,以上面的範例就是把 apple、banana、orange 一一存入 strings 陣列。

過載 (overloading)

或稱多載,是一個 JavaScript 沒有的東西,但 TypeScript 提供來提高 code 維護性與可讀性的機制。

過載可以讓一個函式接受不同數量或型別的參數時去做不一樣的事。

info

Well...怎麼聽起來這麼像聯合型別 + 型別斷言,而且自己寫了幾個例子發現還真的可以這樣幹,於是就疑惑地把問題拋給 chatGPT 了,他是這樣回答我的:

當你的函數有多種輸入和輸出情境,或者需要提供多種配置選項時,使用函數過載可以使你的程式碼更具可讀性和可維護性。

Hmm...這解釋多少有解答我的疑惑,但還是有一種尚未完全撥雲見日的感覺,直到我看到有人這樣解釋:

儘管你能用型別聯集為函式的參數與回傳值定義一系列可用的型別,卻不見得能精確表達出他們彼此之間的關係......多載可以更清楚描述函式的各個型別彼此之間的關係。

套用大佬的範例:

function calculateTax(amount: number | null): number | null {
if (amount !== null) {
return amount * 1.2;
} else {
return null;
}
}

let taxAmount: number | null = calculateTax(100)
if(typeof taxAmount === 'number'){
console.log('Tax value:', taxAmount)
}

calculateTax 是一個接受聯合型別的函式,寫函式的人告訴它:可以接受 numbernull 的型別資料,也可以回傳型別為 numbernull 的值。但使用者並未明確告訴它當參數是 number 時就該回傳 number、是 null 時就該回傳 null

然後用函式的人使用了這個函式,但他並不確定他傳入什麼參數會得到什麼型別的值,所以他還需要寫段 if function 來做型別保護。

所以上面這兩段文字和 code 說了什麼故事?我們可以得到下列資訊:

  1. 寫出函式的人,以及使用函式的人。
  2. 參數與回傳值的型別關係未明確定義。
  3. 兩人有資訊差,導致用函式的人覺得多寫一個型別保護函是比較安全保險。

所以寫函式的那位為了讓用函式的人放心,使用過載來明確定義參數與回傳值型別之間的關係

大佬的過載例子
// 函數的定義
function calculateTax(amount: number): number;
function calculateTax(amount: null): null;

// 函數的實現
function calculateTax(amount: number | null): number | null {
if (amount !== null) {
return amount * 1.2;
} else {
return null;
}
}

// 函式的調用
let taxAmount: number | null = calculateTax(100)
console.log('Tax value:', taxAmount)

這樣在函式的定義就明確訂出參數與回傳值的型別關係,當調用 calculateTax 時,TypeScript 編譯器就會依序查找我們的定義直至匹配的那一個。

參考資料

  1. TypeScript 新手指南
  2. TypeScript 邁向專家之路 / Adam Freeman 著 / 許文達 譯 / 旗標出版