TypeScript 是 JS 類型的超集,並支持了泛型、類型、命名空間、枚舉等特性,彌補了 JS 在大型應用開發中的不足,那麼當 TypeScript 與 React 一塊兒使用會碰撞出怎樣的火花呢?接下來讓咱們一塊兒探索在 TypeScript2.8+ 版本中編寫 React 組件的姿式。前端
近幾年前端對 TypeScript 的呼聲愈來愈高,Ryan Dahl 的新項目 Deno 中 TypeScript 也變成了一個必需要會的技能,知乎上常常見到像『自從用了 TypeScript 以後,不再想用 JavaScript 了』、『只要你用過 ES6,TypeScript 能夠幾乎無門檻接入』、『TypeScript能夠在任何場景代替 JS』這些相似的回答,抱着聽別人說不如本身用的心態逐漸嘗試在團隊內的一些底層支持的項目中使用 TypeScript。node
使用 TypeScript 的編程體驗真的是爽到爆,當在鍵盤上敲下 .
時,後面這一大串的提示真的是滿屏幕的幸福,代碼質量和效率提高十分明顯,不再想用 JavaScript 了。react
在單獨使用 TypeScript 時沒有太大的坑,可是和一些框架結合使用的話坑仍是比較多的,例如使用 React、Vue 這些框架的時候與 TypeScript 的結合會成爲一大障礙,須要去查看框架提供的 .d.ts 的聲明文件中一些複雜類型的定義。本文主要聊一聊與 React 結合時常常遇到的一些類型定義問題,閱讀本文建議對 TypeScript 有必定了解,由於文中對於一些 TypeScript 的基礎的知識不會有太過於詳細的講解。git
import React from 'react'
import ReactDOM from 'react-dom'
const App = () => {
return (
<div>Hello world</div>
)
}
ReactDOM.render(<App />, document.getElementById('root') 複製代碼
上述代碼運行時會出現如下錯誤github
Cannot find module 'react'
typescript
Cannot find module 'react-dom'
shell
錯誤緣由是因爲 React
和 React-dom
並非使用 TS 進行開發的,因此 TS 不知道 React
、 React-dom
的類型,以及該模塊導出了什麼,此時須要引入 .d.ts 的聲明文件,比較幸運的是在社區中已經發布了這些經常使用模塊的聲明文件 DefinitelyTyped 。npm
React
、 React-dom
類型定義文件yarn add @types/react
yarn add @types/react-dom
複製代碼
npm i @types/react -s
npm i @types/react-dom -s
複製代碼
咱們定義一個 App 有狀態組件,props
、 state
以下。編程
props | 類型 | 是否必傳 |
---|---|---|
color |
string | 是 |
size |
string | 否 |
props | 類型 |
---|---|
count |
string |
使用 TSX 咱們能夠這樣寫promise
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 | 類型 | 是否必傳 |
---|---|---|
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 事件對象類型:
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>,
}
複製代碼
在作異步操做時咱們常用 async
函數,函數調用時會 return
一個 Promise
對象,可使用 then
方法添加回調函數。
Promise<T>
是一個泛型類型,T
泛型變量用於肯定使用 then
方法時接收的第一個回調函數(onfulfilled)的參數類型。
實例:
interface IResponse<T> {
message: string,
result: T,
success: boolean,
}
async function getResponse (): Promise<IResponse<number[]>> {
return {
message: '獲取成功',
result: [1, 2, 3],
success: true,
}
}
getResponse()
.then(response => {
console.log(response.result)
})
複製代碼
咱們首先聲明 IResponse
的泛型接口用於定義 response
的類型,經過 T
泛型變量來肯定 result
的類型。
而後聲明瞭一個 異步函數 getResponse
而且將函數返回值的類型定義爲 Promise<IResponse<number[]>>
。
最後調用 getResponse
方法會返回一個 promise
類型,經過 then
調用,此時 then
方法接收的第一個回調函數的參數 response
的類型爲,{ 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
咱們能夠把使用順序倒過來。
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.es5.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.es5.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;
複製代碼
從 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'.
從 T
中提取那些能夠賦值給 U
的類型。
Extract實現源碼 node_modules/typescript/lib/lib.es5.d.ts
。
type Extract<T, U> = T extends U ? T : never;
複製代碼
實例:
type T = Extract<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'.
從 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,
}
複製代碼
將 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',
}
複製代碼
從對象 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: '男'
}
複製代碼
排除 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[]
複製代碼
獲取函數 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
複製代碼