[掘竅] 了解這些,更快掌握 TypeScript 在 React 中的使用(Using TypeScript in React)
tl;dr
$ npm init -y # 初始化 npm
$ npm install -D typescript # 安裝 typescript
$ npx tsc --init # 產生 TS 設定檔 tsconfig.json
內文
這篇文章主要是給想要開始學習 TypeScript,並將 TypeScript 整合進 React 專案中的開發者,也是我自己在學習將 TypeScript 使用於 React 前,很希望有人能夠先提示我的先備知識或觀念。
主要是因為若只是單純想要整合 TypeScript 到 React 中使用,你並不需要了解 TypeScript 中所有的語法就可以開始做這件事,但在學習前若沒有人整理哪些東西需要先看,哪些東西可以跳過之後再補的話,在學習上會顯得有些吃力。因此這篇文章的重點就是去蕪存菁的說明「在 React 中使用 TypeScript 時」需要先知道些什麼,相信閱讀這篇文章後,能夠節省你許多繞圈圈的時間。
在這篇文章中不會說太多關於 TypeScript 語法深入的理論或概念,基本上就是用出來給讀者看,若對於 TypeScript 的語法想要有更多了解,文中會提一些實用的資源,方便讀者深入學習。
簡而言之,這篇文章適合的讀者為:
- 已經具備 React 開發經驗
- 想要認識或學習 TypeScript
- 想要開始在 React 專案中使用 TypeScript
- [加分] 已經看過一些和 TypeScript 有關的文件
那麼就讓我們開始吧!
在 React 專案中會用到的 TypeScript
這裡會很快提一些 TypeScript 的基本語法,如同前面段落所說,這篇文章不會深入 TypeScript 的語法,因此不會提到 TypeScript 中的所有語法,而是會整理筆者在實作 React 專案的過程中,有使用到的基本語法。
為了練習與驗證這些基本語法,我們還是要先建立一個單純的 TypeScript 專案來練習與檢視。
初始化 TypeScript 專案
先建立 typescript-sandbox
這個資料夾,並進入資料夾:
$ mkdir typescript-sandbox
$ cd typescript-sandbox
接著在專案資料夾中透過 npm 安裝 typescript:
$ npm init -y # 初始化 npm
$ npm install -D typescript # 安裝 typescript
當我們安裝了 typescript 這個套件後,在該專案的終端機中就可以使用 tsc
這個指令。
這時候其實就已經可以透過 typescript 提供的 tsc
這個 CLI 工具來編譯 .ts
的檔案了,但一般來說,還會在 TypeScript 的專案中建立一支對應的 TypeScript 設定檔。
要建立 TypeScript 設定檔,只需要執行:
$ npx tsc --init # 產生 TS 設定檔 tsconfig.json
💡 TypeScript 可以選擇要安裝在全域(global)或專案中(local package),這裡因為是安裝在專案資料夾中,所以使用
tsc
指令前要加上npx
的指令,若你的 TypeScript 是安裝在全域環境,可以直接執行tsc --init
。
這時候在專案資料夾中就會多一支名為 tsconfig.json
的檔案:
在這支設定檔中會告訴 TypeScript 該如何去編譯這個專案資料夾中的 TS 檔、專案的根目錄為何、編譯後的檔案要放在哪、撰寫風格的檢查等等,設定的項目非常多,有興趣的讀者可以再參考文末的資源 連結[1],檢視各個設定的含義,這裡單純作為練習和展示使用,並不需要額外調整設定。
執行 TS 檔的程式內容
使用 tsc 編譯 TS 檔案
現在讓我們先來試著使用 TypeScript 提供的 tsc
這個工具來編譯 TS 檔。首先在專案資料夾中新增一支 index.ts
,要留意副檔名是 ts
不是 js
喔!
接著在裡面輸入:
// index.ts
const greet = 'Hello TypeScript';
console.log(greet);
讀者可能會好奇這不就是 JS 的內容嗎?怎麼會說是 TS 呢?這個部分會在後面再多做說明,但這的確就是 TS。
存檔後,在終端機中輸入:
$ npx tsc index.ts
這時候你會看到在專案中多了一支 index.js
檔:
透過 tsc
這個指令,將會把 TS 檔編譯成在 Node.js 可以執行的 JS 檔。現在在終端機中就可以如同執行 JS 檔一樣,去執行編譯好的 JS 檔:
$ node index.js
恭喜你成功執行了第一個 TS 檔的程式內容。
使用 ts-node 直接執行 TS 檔
但這時候你可能會覺得非常麻煩,每次要執行 TypeScript 檔案錢,都必須先用 tsc
指令將 TS 檔編譯成 JS 檔後,再用 node
指令去執行編譯後的 JS 檔,不只感到麻煩,連專案中的檔案都可能變成兩倍(如果是一支 TS 檔,對應產生一支 JS 檔)。
這時候好用的套件 ts-node 來了!
在 Node.js 的環境下我們會使用 node index.js
來執行某支 JS 檔;有了 ts-node,就可以不用先透過 tsc
把 TS 檔編譯成 JS 檔後才能執行,而是可以直接使用 ts-node index.ts
來執行該 TS 檔。
先讓我們在專案中安裝 ts-node,接著使用 ts-node 直接執行 index.ts
:
$ npm install -D ts-node # 安裝 ts-node 到專案資料夾中
$ npx ts-node index.ts # 使用 ts-node 直接執行某支 TS 檔
💡 不論是 typescript 或 ts-node 都可以選擇安裝在全域(global)或專案內(local package),如果是安裝在全域的話,則不需要在最前面加上
npx
,可以直接執行ts-node index.ts
。
現在不需要先將 TS 檔編譯成 JS 檔,便可以直接執行這支 TS 檔,是不是簡潔了許多:
現在你已經知道怎麼執行 TS 的檔案了,接下來讓我們看一下在 React 中常會被使用到的 TypeScript 語法。
React 中常用到的 TypeScript 的基本語法
在還沒撰寫 TypeScript 以前,因為知道它是強型別的語言,所以誤 以為需要主動去對所有變數定義型別,但這句話其實只對一半。在 TypeScript 中,開發者可以「主動」對變數定義型別,但若不去主動定義型別的話,TS 則會幫你「自動」推論這個變數的型別,也就是說,不論有沒有主動定義變數型別,每個變數都將有其明確的型別,當程式執行中,型別有錯誤時就會提出警告。
接著讓我們來看幾個實際的使用範例。
TypeScript 中的型別檢驗
在 TS 中多會在冒號(:
)後定義該變數的型別,例如:
/* 宣告型別為字串或數值 */
let occupation: string; // 宣告 occupation 的型別為 string
以 occupation
這個變數來說,我們主動定義這個變數的型別是字串(string),因此未來只要這個變數帶入的值有不是字串的情況,在 TS 編譯時或 VSCode 的編輯器中都會跳出警告。
我們可以在剛剛的 index.ts
中輸入以下內容:
// index.ts
let occupation: string; // 宣告 occupation 的型別為 string
occupation = 817; // 想要把 occupation 的值帶入數值
這時候 VSCode 會有紅色毛毛蟲在 occupation
下方,把滑鼠移過去時會顯示提示:
這裡的意思是我們要把 817
當作 occupation
這個變數的值,但 occupation
已經定義是字串了,因此 817 並不能作為該變數的值。若我們在終端機執行 npx ts-node index.ts
,終端機一樣會跳出警告,並且無法執行:
但若我們是將 occupation 的值設為 "programmer"
,像是這樣:
let occupation: string;
occupation = 'programmer';
世界就會非常和平,可以正常運行:
這裡我們看到如何將變數的型別定義為字串,接著我們來看一下 TypeScript 中基本的型別。
主動宣告變數型別
字串與數值
在上面的例子中,我們是先定義某個變數,之後才對其賦值,但也可以直接宣告變數型別並賦值,例如:
/* 宣告型別為字串或數值 */
let occupation: string = 'programmer'; // 宣告 occupation 的型別為 string,並且同時賦值
let height: number = 170; // 宣告 height 的型別為 number,並且同時賦值
物件
針對物件型別的定義方式很類似,從下面的例子可以看到在變數 person
後一樣透過 :
的方式,讓 TypeScript 知道 person 是一個物件,其中會有兩個屬性,分別是 name
和 age
,而 name
屬性的型別是 string
,age
屬性的型別則是 number
:
/* 宣告型別為物件 */
const person: { name: string; age: number } = {
name: 'pjchender',
age: 32,
};
物件還有一個比較特別的地方,就是有時物件中的屬性並非一定會存在,例如 person 這個物件,name
和 age
可能是必填,但職業(occupation)可以是選填,也就是可有可無,這時候可以在屬性名稱後方使用 ?
來表示,像是這樣:
/* 在 occupation 後有一個 ?,表示該屬性不一定要存在於該物件中 */
const person: {
name: string;
age: number;
occupation?: string; // 在 : 前面加上 ? 表示該屬性不是必填
} = {
name: 'pjchender',
age: 32,
};
這時候雖然後來賦值時的物件中沒有 occupation
這個屬性,TypeScript 也不會報錯。
陣列
陣列的部分比較特別一些,一般在定義陣列的型別時,除非有額外使用後面會提到的聯集(|
),否則都是定義陣列中所有元素的型別會是相同的。舉例來說,這裡將 devices
這個變數的型別定義為 string[]
,意思就是該陣列中所有的元素都是字串:
/* 宣告型別為字串陣列*/
const devices: string[] = ['iphone', 'pixel', 'ipad', 'note 10'];
如果你 希望該陣列中所有元素都是數值,自然就是使用 number[]
,例如:
/* 宣告型別為數值陣列*/
const luckyNumber: number[] = [4, 7, 11];
你也可能看到有人會寫這樣 Array<number>
,這是使用了後面會講到的泛型(generic)的用法,但意思和上面那行是一樣的:
/* 宣告型別為數值陣列*/
const luckyNumber: Array<number> = [4, 7, 11];
如果陣列中的元素可能包含兩種以上的型別,可以使用聯集(|
)的寫法,這裡的聯集簡單來說就像「或」的概念:
/* 宣告陣列中的元素可以是字串或數值 */
const luckyItem: (string | number)[] = [4, 7, 11, 'phone', 'pad'];
// 另一種寫法
const luckyItem: Array<string | number> = [4, 7, 11, 'phone', 'pad'];
函式
看完了基本的字串、數值、陣列、物件之後,來看一下函式宣告的方式:
/* 宣告型別為函式 */
const add = (x: number, y: number): number => {
return x + y;
};
console.log(add(3, 5)); // 8
這裡可以看到,宣告了一個變數為 add
它的值是函式,這個函式中可以接兩個參數(x
, y
)這兩個參數的型別都是「數值」,在 :
後面則會定義個這函式會「回傳」的型別,這裡一樣定義為數值。
可以用 ts-node
來執行看看這段程式,但若你帶入的參數不是數值的話,VSCode 一樣會跳出紅色毛毛蟲,滑鼠移到毛毛蟲上將會顯示對應的提示:
另外,在函式中很常會使用物件的解構賦值(object destructuring assignment)的方式來取出參數的內容,型別定義的寫法上會像這樣:
/* 宣告型別為函式,參數的地方使用物件的解構賦值 */
const add = ({ x, y }: { x: number; y: number }): number => {
return x + y;
};
add({ x: 3, y: 5 });
有些時候某個函式只是單純執行某些方法,但並不會有實際會回傳值時,我們會在 :
後用 void
這個型別,表示該函式將不會有回傳值,例如下面這個函式會直接在執行時呼叫 console.log
方法,而不會有實際的回傳值,這時候在 :
後方就會使用 void
:
/* 函式不會有回傳值時使用 void */
const add = (x: number, y: number): void => {
console.log(x + y);
};
另外,假設有些方法一定會拋出錯誤(throw error)時,表示函式執行到這裡後就會被中斷而不會繼續運行,這時候會在 :
使用 never
這個型別,意思就是這個函式不會執行完成:
/* 該函式不會執行完成的話,使用 never */
const add = (x: number, y: number): never => {
throw new Error('This is never');
};
add(3, 5);
undefined, null
在 JavaScript 中的 undefined
或 null
也可以被當作型別定義:
/* 宣告型別為 undefined 或 null */
let foo: undefined;
let bar: null;
literal type
除了上述這些比較有「型別味道」的型別之外,實際上型別也可以是定死某個值,例如這裡我們定義 occupation
這個變數的型別就是 "Programmer"
,如此 occupation 這個變數的值就只能是 "Programmer"
,不能是其他的值:
let occupation: 'Programmer';
occupation = 'Programmer';
是不是很有 JS 中常數的味道?
這種型別在 TS 中稱作 literal type[3],它可以是字串或數值,並且經常會搭配聯集(|
)來使用:
let brand: 'iphone' | 'samsung' | 'sony';
這時候 brand
這個變數的值就只能是這三個字串中的其中一種,否則都會報錯。當你使用 VSCode 時,透過其內建的 typescript intellisense,還會自動建議你可以使用的值有哪些:
最後,TypeScript 中的型別不只這些,但上面提到的這些是筆者開發 React 時比較常用到的,若想了解更多關於 TypeScript 的基本型別,可以參考 TypeScript 官網在 Basic Types 的介紹[2]。