2023-11-16|閱讀時間 ‧ 約 14 分鐘

TS 筆記 | 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 的世界。



型別註記

TypeScript, TypeScript…顧名思義型別就是 TypeScript 的重點,它的出現就是為了解決 JavaScript 型別組織鬆散的問題。

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

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

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

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



型別推論

let myName = 'Jeremy'

上面這段例子不會被報錯,會順利編譯出來。Why ~? 不是說 TypeScript 會需要我們做型別註記嗎?啊,難道是它變成剛剛提到的型別 any 了!

錯了!絕對不是 any,如果往下編譯這段 myName = 123,就會看到 TypeScript 噴這段錯誤給你了:Type 'number' is not assignable to type 'string'.

從這個錯誤我們也可以得知,myName 已經被型別註記為 string 了。Why ~~~?

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

也因為型別推論,TypeScript 也建議不用在上述的例子後面自己手動做型別註記,聰明的 TypeScript 會為我們處理好的。



聯合型別

聯合型別就是讓我們的變數可以儲存多種型別中的一種,比如我的 num 變數可以儲存阿拉伯數字或英文:

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

一切正常運作,真是太好了!所以聯合型別就這樣過了吧…才怪,有一個要注意的點:

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

啥意思?看一下範例:

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

Oops,TypeScript 丟了一個錯誤給我們:Property 'length' does not exist on type 'string | number'.

那是因為 length 不是 number prototype 下的方法,但如果是像 toString 這種兩者都有的方法就可以編譯成功囉!



型別斷言

這個神奇的名字在做什麼事情?

答案是用來手動指定一個值的型別。聽起來很抽象吧,舉個例來說,用在前面的聯合型別上。

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

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

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

白話一點就是為參數指定在什麼型別下要幹什麼事。



物件的型別註記

介面

跟基本型別的註記不一樣,物件的型別註記會透過先建立介面 (interfaces) 這個模板來做型別的註記。(在這裡先不要管介面在 OOP 中在幹嘛,我不太懂 OOP,先會用就好 ><)

舉例來說,可以先建個叫 Item 的介面:

interface Item {
name: string,
price: number
}

然後把這個介面作為型別註記在新的物件下:

let phone: Item = {
name: 'IPhone',
price: 34000
}

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

可選屬性

如果今天某個屬性可有可無,比如價格,可以在介面以可選屬性來為它做定義:

price?: number

這樣就解決不能少放屬性的問題了。

任意屬性

那我想多放屬性呢?這時就要用任意屬性了。

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]

這樣寫就是明確聲明這是一個存數字的陣列。



函式的型別

JavaScript 作為 FP 的經典語言,函式是這個國度的一等公民。

最一開始的時候寫過一個例子:

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

這就是一個典型的函式型別:定義參數接收型別、定義 return 回傳值型別。

可選參數

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

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

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

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

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

這時 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 維護性與可讀性的機制。

過載可以讓一個函式接受不同數量或型別的參數時去做不一樣的事。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 著 / 許文達 譯 / 旗標出版


分享至
成為作者繼續創作的動力吧!
這個專題用來存放我在學習網頁開發時的心得及知識。
從 Google News 追蹤更多 vocus 的最新精選內容從 Google News 追蹤更多 vocus 的最新精選內容

發表回應

成為會員 後即可發表留言
© 2024 vocus All rights reserved.