TypeScript 在 React 中使用總結

近幾年前端對 TypeScript 的呼聲越來越高,Ryan Dahl 的新專案 Deno 中 TypeScript 也變成了一個必須要會的技能,知乎上經常見到像『自從用了 TypeScript 之後,再也不想用 JavaScript 了』、『只要你用過 ES6,TypeScript 可以幾乎無門檻接入』、『TypeScript可以在任何場景代替 JS』這些類似的回答,抱著聽別人說不如自己用的心態逐漸嘗試在團隊內的一些底層支援的專案中使用 TypeScript。
使用 TypeScript 的程式設計體驗真的是爽到爆,當在鍵盤上敲下 .
時,後面這一大串的提示真的是滿螢幕的幸福,程式碼質量和效率提升十分明顯,再也不想用 JavaScript 了。
在單獨使用 TypeScript 時沒有太大的坑,但是和一些框架結合使用的話坑還是比較多的,例如使用 React、Vue 這些框架的時候與 TypeScript 的結合會成為一大障礙,需要去檢視框架提供的 .d.ts 的宣告檔案中一些複雜型別的定義。本文主要聊一聊與 React 結合時經常遇到的一些型別定義問題,閱讀本文建議對 TypeScript 有一定了解,因為文中對於一些 TypeScript 的基礎的知識不會有太過於詳細的講解。
編寫第一個 TSX 元件
import React from 'react' import ReactDOM from 'react-dom' const App = () => { return ( <div>Hello world</div> ) } ReactDOM.render(<App />, document.getElementById('root') 複製程式碼
上述程式碼執行時會出現以下錯誤
-
Cannot find module 'react'
-
Cannot find module 'react-dom'
錯誤原因是由於 React
和 React-dom
並不是使用 TS 進行開發的,所以 TS 不知道 React
、 React-dom
的型別,以及該模組匯出了什麼,此時需要引入 .d.ts 的宣告檔案,比較幸運的是在社群中已經發布了這些常用模組的宣告檔案DefinitelyTyped 。
安裝 React
、 React-dom
型別定義檔案
使用 yarn 安裝
yarn add @types/react yarn add @types/react-dom 複製程式碼
使用 npm 安裝
npm i @types/react -s npm i @types/react-dom -s 複製程式碼
有狀態元件開發
我們定義一個 App 有狀態元件, props
、 state
如下。
Props
props | 型別 | 是否必傳 |
---|---|---|
color |
string | 是 |
size |
string | 否 |
State
props | 型別 |
---|---|
count |
string |
使用 TSX 我們可以這樣寫
import * as React from 'react' interface IProps { color: string, size?: string, } interface IState { count: number, } class App extends React.Component<IProps, IState> { public state = { count: 1, } public render () { return ( <div>Hello world</div> ) } } 複製程式碼
TypeScript 可以對 JSX 進行解析,充分利用其本身的靜態檢查功能,使用泛型進行 Props
、 State
的型別定義。定義後在使用 this.state
和 this.props
時可以在編輯器中獲得更好的智慧提示,並且會對型別進行檢查。
那麼 Component 的泛型是如何實現的呢,我們可以參考下 React 的型別定義檔案 node_modules/@types/react/index.d.ts
。
在這裡可以看到 Component
這個泛型類, P
代表 Props
的型別, S
代表 State
的型別。
class Component<P, S> { readonly props: Readonly<{ children?: ReactNode }> & Readonly<P>; state: Readonly<S>; } 複製程式碼
Component 泛型類在接收到 P
, S
這兩個泛型變數後,將只讀屬性 props
的型別宣告為交叉型別 Readonly<{ children?: ReactNode }> & Readonly<P>;
使其支援 children
以及我們宣告的 color
、 size
。
通過泛型的類型別名 Readonly
將 props
的所有屬性都設定為只讀屬性。
Readonly 實現原始碼 node_modules/typescript/lib/lib.es5.d.ts
。
由於 props
屬性被設定為只讀,所以通過 this.props.size = 'sm'
進行更新時候 TS 檢查器會進行錯誤提示, Error:(23, 16) TS2540: Cannot assign to 'size' because it is a constant or a read-only property
防止直接更新 state
React的 state
更新需要使用 setState
方法,但是我們經常誤操作,直接對 state
的屬性進行更新。
this.state.count = 2 複製程式碼
開發中有時候會不小心就會寫出上面這種程式碼,執行後 state
並沒有更新,我們此時會特別抓狂,心裡想著我哪裡又錯了?
現在有了 TypeScript 我們可以通過將 state
,以及 state
下面的屬性都設定為只讀型別,從而防止直接更新 state
。
import * as React from 'react' interface IProps { color: string, size?: string, } interface IState { count: number, } class App extends React.PureComponent<IProps, IState> { public readonly state: Readonly<IState> = { count: 1, } public render () { return ( <div>Hello world</div> ) } public componentDidMount () { this.state.count = 2 } } export default App 複製程式碼
此時我們直接修改 state
值的時候 TypeScript 會立刻告訴我們錯誤, Error:(23, 16) TS2540: Cannot assign to 'count' because it is a constant or a read-only property.
。
無狀態元件開發
Props
props | 型別 | 是否必傳 |
---|---|---|
children |
ReactNode | 否 |
onClick |
function | 是 |
SFC
型別
在 React 的宣告檔案中 已經定義了一個 SFC
型別,使用這個型別可以避免我們重複定義 children
、 propTypes
、 contextTypes
、 defaultProps
、 displayName
的型別。
實現原始碼 node_modules/@types/react/index.d.ts
。
type SFC<P = {}> = StatelessComponent<P>; interface StatelessComponent<P = {}> { (props: P & { children?: ReactNode }, context?: any): ReactElement<any> | null; propTypes?: ValidationMap<P>; contextTypes?: ValidationMap<any>; defaultProps?: Partial<P>; displayName?: string; } 複製程式碼
使用 SFC
進行無狀態元件開發。
import { SFC } from 'react' import { MouseEvent } from 'react' import * as React from 'react' interface IProps { onClick (event: MouseEvent<HTMLDivElement>): void, } const Button: SFC<IProps> = ({onClick, children}) => { return ( <div onClick={onClick}> { children } </div> ) } export default Button 複製程式碼
事件處理
我們在進行事件註冊時經常會在事件處理函式中使用 event
事件物件,例如當使用滑鼠事件時我們通過 clientX
、 clientY
去獲取指標的座標。
大家可以想到直接把 event
設定為 any
型別,但是這樣就失去了我們對程式碼進行靜態檢查的意義。
function handleEvent (event: any) { console.log(event.clientY) } 複製程式碼
試想下當我們註冊一個 Touch
事件,然後錯誤的通過事件處理函式中的 event
物件去獲取其 clientY
屬性的值,在這裡我們已經將 event
設定為 any
型別,導致 TypeScript 在編譯時並不會提示我們錯誤, 當我們通過 event.clientY
訪問時就有問題了,因為 Touch
事件的 event
物件並沒有 clientY
這個屬性。
通過 interface
對 event
物件進行型別宣告編寫的話又十分浪費時間,幸運的是 React 的宣告檔案提供了 Event
物件的型別宣告。
Event 事件物件型別
常用 Event 事件物件型別:
-
ClipboardEvent<T = Element>
剪貼簿事件物件 -
DragEvent<T = Element>
拖拽事件物件 -
ChangeEvent<T = Element>
Change 事件物件 -
KeyboardEvent<T = Element>
鍵盤事件物件 -
MouseEvent<T = Element>
滑鼠事件物件 -
TouchEvent<T = Element>
觸控事件物件 -
WheelEvent<T = Element>
滾輪事件物件 -
AnimationEvent<T = Element>
動畫事件物件 -
TransitionEvent<T = Element>
過渡事件物件
例項:
import { MouseEvent } from 'react' interface IProps { onClick (event: MouseEvent<HTMLDivElement>): void, } 複製程式碼
MouseEvent
型別實現原始碼 node_modules/@types/react/index.d.ts
。
interface SyntheticEvent<T = Element> { bubbles: boolean; /** * A reference to the element on which the event listener is registered. */ currentTarget: EventTarget & T; cancelable: boolean; defaultPrevented: boolean; eventPhase: number; isTrusted: boolean; nativeEvent: Event; preventDefault(): void; isDefaultPrevented(): boolean; stopPropagation(): void; isPropagationStopped(): boolean; persist(): void; // If you thought this should be `EventTarget & T`, see https://github.com/DefinitelyTyped/DefinitelyTyped/pull/12239 /** * A reference to the element from which the event was originally dispatched. * This might be a child element to the element on which the event listener is registered. * * @see currentTarget */ target: EventTarget; timeStamp: number; type: string; } interface MouseEvent<T = Element> extends SyntheticEvent<T> { altKey: boolean; button: number; buttons: number; clientX: number; clientY: number; ctrlKey: boolean; /** * See [DOM Level 3 Events spec](https://www.w3.org/TR/uievents-key/#keys-modifier). for a list of valid (case-sensitive) arguments to this method. */ getModifierState(key: string): boolean; metaKey: boolean; nativeEvent: NativeMouseEvent; pageX: number; pageY: number; relatedTarget: EventTarget; screenX: number; screenY: number; shiftKey: boolean; } 複製程式碼
EventTarget
型別實現原始碼 node_modules/typescript/lib/lib.dom.d.ts
。
interface EventTarget { addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void; dispatchEvent(evt: Event): boolean; removeEventListener(type: string, listener?: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void; } 複製程式碼
通過原始碼我們可以看到 MouseEvent<T = Element>
繼承 SyntheticEvent<T>
,並且通過 T
接收一個 DOM
元素的型別, currentTarget
的型別由 EventTarget & T
組成交叉型別。
事件處理函式型別
當我們定義事件處理函式時有沒有更方便定義其函式型別的方式呢?答案是使用 React 宣告檔案所提供的 EventHandler
類型別名,通過不同事件的 EventHandler
的類型別名來定義事件處理函式的型別。
EventHandler
型別實現原始碼 node_modules/@types/react/index.d.ts
。
type EventHandler<E extends SyntheticEvent<any>> = { bivarianceHack(event: E): void }["bivarianceHack"]; type ReactEventHandler<T = Element> = EventHandler<SyntheticEvent<T>>; type ClipboardEventHandler<T = Element> = EventHandler<ClipboardEvent<T>>; type DragEventHandler<T = Element> = EventHandler<DragEvent<T>>; type FocusEventHandler<T = Element> = EventHandler<FocusEvent<T>>; type FormEventHandler<T = Element> = EventHandler<FormEvent<T>>; type ChangeEventHandler<T = Element> = EventHandler<ChangeEvent<T>>; type KeyboardEventHandler<T = Element> = EventHandler<KeyboardEvent<T>>; type MouseEventHandler<T = Element> = EventHandler<MouseEvent<T>>; type TouchEventHandler<T = Element> = EventHandler<TouchEvent<T>>; type PointerEventHandler<T = Element> = EventHandler<PointerEvent<T>>; type UIEventHandler<T = Element> = EventHandler<UIEvent<T>>; type WheelEventHandler<T = Element> = EventHandler<WheelEvent<T>>; type AnimationEventHandler<T = Element> = EventHandler<AnimationEvent<T>>; type TransitionEventHandler<T = Element> = EventHandler<TransitionEvent<T>>; 複製程式碼
EventHandler
接收 E
,其代表事件處理函式中 event
物件的型別。
bivarianceHack
為事件處理函式的型別定義,函式接收一個 event
物件,並且其型別為接收到的泛型變數 E
的型別, 返回值為 void
。
例項:
interface IProps { onClick : MouseEventHandler<HTMLDivElement>, } 複製程式碼
Promise 型別
在做非同步操作時我們經常使用 async
函式,函式呼叫時會 return
一個 Promise
物件,可以使用 then
方法添加回調函式。
Promise<T>
是一個泛型型別, T
泛型變數用於確定使用 then
方法時接收的第一個回撥函式(onfulfilled)的引數型別。
例項:
interface IResponse<T> { message: string, result: T, success: boolean, } async function getResult (): Promise<IResponse<number[]>> { return { message: '獲取成功', result: [1, 2, 3], success: true, } } getResult() .then(result => { console.log(result.result) }) 複製程式碼
我們首先宣告 IResponse
的泛型介面用於定義 response
的型別,通過 T
泛型變數來確定 result
的型別。
然後聲明瞭一個 非同步函式 getResult
並且將函式返回值的型別定義為 Promise<IResponse<number[]>>
。
最後呼叫 getResult
方法會返回一個 promise
型別,通過 .then
呼叫,此時 .then
方法接收的第一個回撥函式的引數 result
的型別為, { message: string, result: number[], success: boolean}
。
Promise<T>
實現原始碼 node_modules/typescript/lib/lib.es5.d.ts
。
interface Promise<T> { /** * Attaches callbacks for the resolution and/or rejection of the Promise. * @param onfulfilled The callback to execute when the Promise is resolved. * @param onrejected The callback to execute when the Promise is rejected. * @returns A Promise for the completion of which ever callback is executed. */ then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>; /** * Attaches a callback for only the rejection of the Promise. * @param onrejected The callback to execute when the Promise is rejected. * @returns A Promise for the completion of the callback. */ catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>; } 複製程式碼
工具泛型使用技巧
typeof
一般我們都是先定義型別,再去賦值使用,但是使用 typeof
我們可以把使用順序倒過來。
const options = { a: 1 } type Options = typeof options 複製程式碼
使用字串字面量型別限制值為固定的字串引數
限制 props.color
的值只可以是字串 red
、 blue
、 yellow
。
interface IProps { color: 'red' | 'blue' | 'yellow', } 複製程式碼
使用數字字面量型別限制值為固定的數值引數
限制 props.index
的值只可以是數字 0
、 1
、 2
。
interface IProps { index: 0 | 1 | 2, } 複製程式碼
使用 Partial
將所有的 props
屬性都變為可選值
Partial
實現原始碼 node_modules/typescript/lib/lib.dom.d.ts
type Partial<T> = { [P in keyof T]?: T[P] }; 複製程式碼
上面程式碼的意思是 keyof T
拿到 T
所有屬性名, 然後 in
進行遍歷, 將值賦給 P
, 最後 T[P]
取得相應屬性的值,中間的 ?
用來進行設定為可選值。
如果 props
所有的屬性值都是可選的我們可以藉助 Partial
這樣實現。
import { MouseEvent } from 'react' import * as React from 'react' interface IProps { color: 'red' | 'blue' | 'yellow', onClick (event: MouseEvent<HTMLDivElement>): void, } const Button: SFC<Partial<IProps>> = ({onClick, children, color}) => { return ( <div onClick={onClick}> { children } </div> ) 複製程式碼
使用 Required
將所有 props
屬性都設為必填項
Required
實現原始碼 node_modules/typescript/lib/lib.dom.d.ts
。
type Required<T> = { [P in keyof T]-?: T[P] }; 複製程式碼
看到這裡,小夥伴們可能有些疑惑, -?
是做什麼的,其實 -?
的功能就是把 ?
去掉變成可選項,對應的還有 +?
,作用與 -?
相反,是把屬性變為可選項。
條件型別
TypeScript2.8引入了條件型別,條件型別可以根據其他型別的特性做出型別的判斷。
T extends U ? X : Y 複製程式碼
原先
interface Id { id: number, /* other fields */ } interface Name { name: string, /* other fields */ } declare function createLabel(id: number): Id; declare function createLabel(name: string): Name; declare function createLabel(name: string | number): Id | Name; 複製程式碼
使用條件型別
type IdOrName<T extends number | string> = T extends number ? Id : Name; declare function createLabel<T extends number | string>(idOrName: T): T extends number ? Id : Name; 複製程式碼
Exclude<T,U>
從 T
中排除那些可以賦值給 U
的型別。
Exclude
實現原始碼 node_modules/typescript/lib/lib.es5.d.ts
。
type Exclude<T, U> = T extends U ? never : T; 複製程式碼
例項:
type T = Exclude<1|2|3|4|5, 3|4>// T = 1|2|5 複製程式碼
此時 T
型別的值只可以為 1
、 2
、 5
,當使用其他值是 TS 會進行錯誤提示。
Error:(8, 5) TS2322: Type '3' is not assignable to type '1 | 2 | 5'.
Extract<T,U>
從 T
中提取那些可以賦值給 U
的型別。
Extract實現原始碼 node_modules/typescript/lib/lib.es5.d.ts
。
type Extract<T, U> = T extends U ? T : never; 複製程式碼
例項:
type T = Exclude<1|2|3|4|5, 3|4>// T = 3|4 複製程式碼
此時T型別的值只可以為 3
、 4
,當使用其他值時 TS 會進行錯誤提示:
Error:(8, 5) TS2322: Type '5' is not assignable to type '3 | 4'.
Pick<T,K>
從 T
中取出一系列 K
的屬性。
Pick
實現原始碼 node_modules/typescript/lib/lib.es5.d.ts
。
type Pick<T, K extends keyof T> = { [P in K]: T[P]; }; 複製程式碼
例項:
假如我們現在有一個型別其擁有 name
、 age
、 sex
屬性,當我們想生成一個新的型別只支援 name
、 age
時可以像下面這樣:
interface Person { name: string, age: number, sex: string, } let person: Pick<Person, 'name' | 'age'> = { name: '小王', age: 21, } 複製程式碼
Record<K,T>
將 K
中所有的屬性的值轉化為 T
型別。
Record
實現原始碼 node_modules/typescript/lib/lib.es5.d.ts
。
type Record<K extends keyof any, T> = { [P in K]: T; }; 複製程式碼
例項:
將 name
、 age
屬性全部設為 string
型別。
let person: Record<'name' | 'age', string> = { name: '小王', age: '12', } 複製程式碼
Omit<T,K>(沒有內建)
從物件 T
中排除 key
是 K
的屬性。
由於 TS 中沒有內建,所以需要我們使用 Pick
和 Exclude
進行實現。
type Omit<T, K> = Pick<T, Exclude<keyof T, K>> 複製程式碼
例項:
排除 name
屬性。
interface Person { name: string, age: number, sex: string, } let person: Omit<Person, 'name'> = { age: 1, sex: '男' } 複製程式碼
NonNullable <T>
排除 T
為 null
、 undefined
。
NonNullable
實現原始碼 node_modules/typescript/lib/lib.es5.d.ts
。
type NonNullable<T> = T extends null | undefined ? never : T; 複製程式碼
例項:
type T = NonNullable<string | string[] | null | undefined>; // string | string[] 複製程式碼
ReturnType<T>
獲取函式 T
返回值的型別。。
ReturnType
實現原始碼 node_modules/typescript/lib/lib.es5.d.ts
。
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any; 複製程式碼
infer R
相當於宣告一個變數,接收傳入函式的返回值型別。
例項:
type T1 = ReturnType<() => string>; // string type T2 = ReturnType<(s: string) => void>; // void 複製程式碼