3. 優美地定義 React 型別


由於許多人應該是沒有寫過 TypeScript
於是這邊會簡單介紹 TypeScript 的基礎
如果想知道更深可以參照官網

在剛開始學習 TypeScript 的過程中
一定會想說從官方的範例開始照著實作
後來發現這是夢魘的開始啊~~~
TypeScript 的官方範例非常的完(艱)整(澀)
所以比起啃那些文件,多數人應該是像我一樣 Stack Overflow 學習法

副檔名

TypeScript 有兩種檔案:.ts.d.ts
.ts 副檔名的檔案就是 TypeScript 檔案(廢話)
.d.ts 副檔名則是代表型別的定義檔案
一般來說,我們會將自定義的型別放到 .d.ts 裡面

除了這兩種檔案之外,開發 React 時,會習慣使用 .tsx 來撰寫
如同 .jsx 一般,讓開發時可以更清楚了解這個檔案是 React 的元件
或許有些開發者會覺得不需要區分 .js / .jsx
但對於開發上的溝通來說,還是建議標示比較合適

型別定義

我們之所以喜歡 TypeScript,在於它可以良好地定義我們想要的型別:

type User = {
    name: string
    age: number
    picture: string | null
    role: "admin" | "member" | "guest"
    description?: string
}

上面的自訂型別說明了:

  • 有一個名為 User 的型別
  • User 型別裡面有 name, age, picture, description 屬性
  • picture 可為 string 或是 null
  • role 只可為 "admin", "member" 或是 "guest"
  • description 可以沒有(undefined)

於是我們就可以像這樣子使用:

const addUser = (user: User) => {
    console.log(user.name)
    console.log(user.picture) // 可能為 null
    console.log(user.role) // 只可能為 admin, member, guest
    console.log(user.description) // 可能為 undefined
    console.log(user.foo) // 會編譯失敗
}

有了上面的自訂型別後,我們就可以避免惱人的 TypeError: Cannot call method 'xxx' of undefined,更嚴謹與優美地撰寫我們的程式

這邊刻意不提到 interface,建議直接使用 type 來定義自訂型別即可(用更 functional 思維)
可參考:interface v.s. type

至於 enum 的話,在一般情況我們是不需要使用的,直接使用字串即可,例如:

type Currency = "NTD" | "USD"

只有在一種情形下建議使用 enum,那就是我們需要使用 reverse mapping
也就是有 key 跟 value,且需要從 value 對應回 key 的情況,舉例來說:

enum Reward {
    Poor = 1,
    Soso = 3,
    Good = 5,
}
Reward.Good -> 5
Reward[3] -> "Soso"

不過還是建議減少 reverse mapping 的用法,主要是會讓程式碼的可讀性下降不少
到目前為止,許多套件還是以 type 使用為主

React 中實際使用

相信大家最卡關的應該就是這個部分
使用在 React 裡面時到底該如何定義型別?
我們這邊以各情況來進行說明

定義 Functional Component

type ButtonProps = {
    text: string
}
const Button: React.FC<ButtonProps> = ({ text }) => {
    return <button>{text}</button>
}
export default Button

首先需要先定義 Props,再來定義元件
元件請使用 React 定義好的 FunctionalComponent 型別(簡寫 FC)
在 TypeScript 中的 <> 相當於 function 中的 ()
我們稱為型別參數,可以想成是 function 中的參數
所以 React.FC<ButtonProps> 代表定義一個 Props 為 ButtonProps 的 FC

定義 Event Handler

const App = () => {
    const handleClick: React.MouseEventHandler<HTMLButtonElement> = e => {
        console.log(e)
    }
    return <button onClick={handleClick}>Click</button>
}

在 TypeScript in React 中最難的就是 EventHandler 的定義
這邊拆成兩部分解釋:

  1. Event Handler
    我們必須先選擇這是要處理哪種 Event 的 Handler,Event 種類有很多,都是以 Event 結尾,像是 MouseEvent、FormEvent、KeyboardEvent 等等,可參考 React 官方文件
  2. Target Element
    接下來我們必須定義 Handler 的作用目標是什麼,以範例來說,我們要處理的是作用在「按鈕」上面的點擊事件,所以我們必需要使用 HTMLButtonElement 來宣告我們的作用目標,作用目標有很多種,都是 HTML 開頭,Element 結尾,像是 HTMLFormElement, HTMLDivElement 等等,可參考 MDN

這邊建議使用

const handler: EventHandler = e => {}

而不是

const handler = (e: Event) => {}

主要是因為我們盡量使用 React 已經幫我們定義好的型別,避免自己重複定義 Handler 型別

定義共用型別

type UserRole = "admin" | "member" | "guest"

像這種型別常常會跨元件共用,這邊建議放到 src/types 底下
並且依照功能區分,例如:
跟使用者相關的放到 src/types/user.d.ts
跟付款相關的放到 src/types/payment.d.ts
這邊記得要使用 .d.ts 作為副檔名,這樣在編譯時才會自動將型別放進去

PS. create-react-app v3 是使用 TypeScript 3.7,有提供 optional chaining 的語法,具體用法如下:

// TypeScript 3.7 以前
foo && foo.bar && foo.bar.baz
// TypeScript 3.7 以後
foo?.bar?.baz

結語

型別定義的部分其實有更多部分可以講
但是如果全部打完的話,這篇大概就是我的最後一篇了
所以之後會以實際案例來介紹,希望這篇能稍微幫上點忙

murmur...
還有四天,快過半了!!!








你可能感興趣的文章

HTML訓導處|<b><strong><i><em>標籤的差別

HTML訓導處|<b><strong><i><em>標籤的差別

1/26 開發日記

1/26 開發日記

[JavaScript] 用 Jest 做單元測試

[JavaScript] 用 Jest 做單元測試






留言討論