如何優雅地在 React 中使用TypeScript,看這一篇就夠了!

語言: CN / TW / HK

theme: fancy

畢業已有3月有餘,工作用的技術棧主要是React hooks + TypeScript。其實在單獨使用 TypeScript 時沒有太多的坑,不過和React結合之後就會複雜很多。本文就來聊一聊TypeScript與React一起使用時經常遇到的一些型別定義的問題。閱讀本文前,希望你能有一定的React和TypeScript基礎。文章內容較多,建議先收藏再學習!

一、元件宣告

在React中,元件的宣告方式有兩種:函式元件類元件, 來看看這兩種型別的元件宣告時是如何定義TS型別的。

1. 類元件

類元件的定義形式有兩種:React.Component<P, S={}>React.PureComponent<P, S={} SS={}>,它們都是泛型介面,接收兩個引數,第一個是props型別的定義,第二個是state型別的定義,這兩個引數都不是必須的,沒有時可以省略: ```typescript interface IProps { name: string; }

三連 .gif interface IState { count: number; }

class App extends React.Component { state = { count: 0 };

render() { return (

{this.state.count} {this.props.name}
); } }

export default App; `React.PureComponent<P, S={} SS={}>` 也是差不多的:typescript class App extends React.PureComponent {} ``React.PureComponent是有第三個引數的,它表示getSnapshotBeforeUpdate`的返回值。

那PureComponent和Component 的區別是什麼呢?它們的主要區別是PureComponent中的shouldComponentUpdate 是由自身進行處理的,不需要我們自己處理,所以PureComponent可以在一定程度上提升效能。

有時候可能會見到這種寫法,實際上和上面的效果是一樣的: ```typescript import React, {PureComponent, Component} from "react";

class App extends PureComponent {}

class App extends Component {} 那如果定義時候我們不知道元件的props的型別,只有在呼叫時才知道元件型別,該怎麼辦呢?這時泛型就發揮作用了:typescript // 定義元件 class MyComponent

extends React.Component

{ internalProp: P; constructor(props: P) { super(props); this.internalProp = props; } render() { return ( hello world ); } }

// 使用元件 type IProps = { name: string; age: number; };

name="React" age={18} />; // Success name="TypeScript" age="hello" />; // Error ```

2. 函式元件

通常情況下,函式元件我是這樣寫的: ```typescript interface IProps { name: string }

const App = (props: IProps) => { const {name} = props;

return (

hello world

{name}

); }

export default App; 除此之外,函式型別還可以使用`React.FunctionComponent<P={}>`來定義,也可以使用其簡寫`React.FC<P={}>`,兩者效果是一樣的。它是一個泛型介面,可以接收一個引數,引數表示props的型別,這個引數不是必須的。它們就相當於這樣:typescript type React.FC

= React.FunctionComponent

最終的定義形式如下:typescript interface IProps { name: string }

const App: React.FC = (props) => { const {name} = props; return (

hello world

{name}

); }

export default App; 當使用這種形式來定義函式元件時,props中預設會帶有children屬性,它表示該元件在呼叫時,其內部的元素,來看一個例子,首先定義一個元件,元件中引入了Child1和Child2元件:typescript import Child1 from "./child1"; import Child2 from "./child2";

interface IProps { name: string; } const App: React.FC = (props) => { const { name } = props; return ( TypeScript ); };

export default App; Child1元件結構如下:typescript interface IProps { name: string; } const Child1: React.FC = (props) => { const { name, children } = props; console.log(children); return (

hello child1

{name}

); };

export default Child1; ``` 我們在Child1元件中列印了children屬性,它的值是一個數組,包含Child2物件和後面的文字:

image.png

使用 React.FC 宣告函式元件和普通宣告的區別如下:

  • React.FC 顯式地定義了返回型別,其他方式是隱式推導的;
  • React.FC 對靜態屬性:displayName、propTypes、defaultProps 提供了型別檢查和自動補全;
  • React.FC 為 children 提供了隱式的型別(ReactElement | null)。

那如果我們在定義元件時不知道props的型別,只有呼叫時才知道,那就還是用泛型來定義props的型別。對於使用function定義的函式元件: ```typescript // 定義元件 function MyComponent

(props: P) { return ( {props} ); }

// 使用元件 type IProps = { name: string; age: number; };

name="React" age={18} />; // Success name="TypeScript" age="hello" />; // Error 如果使用箭頭函式定義的函式元件,直接這樣呼叫時錯誤的:typescript const MyComponent =

(props: P) { return ( {props} ); } 必須使用extends關鍵字來定義泛型引數才能被成功解析:typescript const MyComponent =

(props: P) { return ( {props} ); } ```

二、React內建型別

1. JSX.Element

先來看看JSX.Element型別的宣告: typescript declare global { namespace JSX { interface Element extends React.ReactElement<any, any> { } } } 可以看到,JSX.Element是ReactElement的子型別,它沒有增加屬性,兩者是等價的。也就是說兩種型別的變數可以相互賦值。 ​

JSX.Element 可以通過執行 React.createElement 或是轉譯 JSX 獲得: typescript const jsx = <div>hello</div> const ele = React.createElement("div", null, "hello");

2. React.ReactElement

React 的型別宣告檔案中提供了 React.ReactElement<T>,它可以讓我們通過傳入<T/>來註解類元件的例項化,它在宣告檔案中的定義如下: typescript interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> { type: T; props: P; key: Key | null; } ReactElement是一個介面,包含type,props,key三個屬性值。該型別的變數值只能是兩種: null 和 ReactElement例項。 ​

通常情況下,函式元件返回ReactElement(JXS.Element)的值。

3. React.ReactNode

ReactNode型別的宣告如下: ```typescript type ReactText = string | number; type ReactChild = ReactElement | ReactText;

interface ReactNodeArray extends Array {} type ReactFragment = {} | ReactNodeArray; type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined; ``` 可以看到,ReactNode是一個聯合型別,它可以是string、number、ReactElement、null、boolean、ReactNodeArray。由此可知。ReactElement型別的變數可以直接賦值給ReactNode型別的變數,但反過來是不行的。

類元件的 render 成員函式會返回 ReactNode 型別的值: typescript class MyComponent extends React.Component { render() { return <div>hello world</div> } } // 正確 const component: React.ReactNode<MyComponent> = <MyComponent />; // 錯誤 const component: React.ReactNode<MyComponent> = <OtherComponent />; 上面的程式碼中,給component變數設定了型別是Mycomponent型別的react例項,這時只能給其賦值其為MyComponent的例項元件。 ​

通常情況下,類元件通過 render() 返回 ReactNode的值。

4. CSSProperties

先來看看React的宣告檔案中對CSSProperties 的定義: typescript export interface CSSProperties extends CSS.Properties<string | number> { /** * The index signature was removed to enable closed typing for style * using CSSType. You're able to use type assertion or module augmentation * to add properties or an index signature of your own. * * For examples and more information, visit: * https://github.com/frenic/csstype#what-should-i-do-when-i-get-type-errors */ } React.CSSProperties是React基於TypeScript定義的CSS屬性型別,可以將一個方法的返回值設定為該型別: ```typescript import * as React from "react";

const classNames = require("./sidebar.css");

interface Props { isVisible: boolean; }

const divStyle = (props: Props): React.CSSProperties => ({ width: props.isVisible ? "23rem" : "0rem" });

export const SidebarComponent: React.StatelessComponent = props => (

{props.children}

); ``` 這裡divStyle元件的返回值就是React.CSSProperties型別。

我們還可以定義一個CSSProperties型別的變數: typescript const divStyle: React.CSSProperties = { width: "11rem", height: "7rem", backgroundColor: `rgb(${props.color.red},${props.color.green}, ${props.color.blue})` }; 這個變數可以在HTML標籤的style屬性上使用: ```typescript

在React的型別宣告檔案中,style屬性的型別如下:typescript style?: CSSProperties | undefined; ```

三、React Hooks

1. useState

預設情況下,React會為根據設定的state的初始值來自動推導state以及更新函式的型別:

image.png

如果已知state 的型別,可以通過以下形式來自定義state的型別: typescript const [count, setCount] = useState<number>(1) 如果初始值為null,需要顯式地宣告 state 的型別: typescript const [count, setCount] = useState<number | null>(null); 如果state是一個物件,想要初始化一個空物件,可以使用斷言來處理: typescript const [user, setUser] = React.useState<IUser>({} as IUser); 實際上,這裡將空物件{}斷言為IUser介面就是欺騙了TypeScript的編譯器,由於後面的程式碼可能會依賴這個物件,所以應該在使用前及時初始化 user 的值,否則就會報錯。

下面是宣告檔案中 useState 的定義: ```typescript function useState(initialState: S | (() => S)): [S, Dispatch>]; // convenience overload when first argument is omitted /* * Returns a stateful value, and a function to update it. * * @version 16.8.0 * @see https://reactjs.org/docs/hooks-reference.html#usestate /

function useState(): [S | undefined, Dispatch>]; /* * An alternative to useState. * * useReducer is usually preferable to useState when you have complex state logic that involves * multiple sub-values. It also lets you optimize performance for components that trigger deep * updates because you can pass dispatch down instead of callbacks. * * @version 16.8.0 * @see https://reactjs.org/docs/hooks-reference.html#usereducer / ``` 可以看到,這裡定義兩種形式,分別是有初始值和沒有初始值的形式。

2. useEffect

useEffect的主要作用就是處理副作用,它的第一個引數是一個函式,表示要清除副作用的操作,第二個引數是一組值,當這組值改變時,第一個引數的函式才會執行,這讓我們可以控制何時執行函式來處理副作用: typescript useEffect( () => { const subscription = props.source.subscribe(); return () => { subscription.unsubscribe(); }; }, [props.source] ); 當函式的返回值不是函式或者effect函式中未定義的內容時,如下: typescript useEffect( () => { subscribe(); return null; } ); TypeScript就會報錯:

image.png

來看看useEffect在型別宣告檔案中的定義: ```typescript // Destructors are only allowed to return void. type Destructor = () => void | { [UNDEFINED_VOID_ONLY]: never };

// NOTE: callbacks are only allowed to return either void, or a destructor. type EffectCallback = () => (void | Destructor);

// TODO (TypeScript 3.0): ReadonlyArray type DependencyList = ReadonlyArray;

function useEffect(effect: EffectCallback, deps?: DependencyList): void; // NOTE: this does not accept strings, but this will have to be fixed by removing strings from type Ref /* * useImperativeHandle customizes the instance value that is exposed to parent components when using * ref. As always, imperative code using refs should be avoided in most cases. * * useImperativeHandle should be used with React.forwardRef. * * @version 16.8.0 * @see https://reactjs.org/docs/hooks-reference.html#useimperativehandle / ``` 可以看到,useEffect的第一個引數只允許返回一個函式。

3. useRef

當使用 useRef 時,我們可以訪問一個可變的引用物件。可以將初始值傳遞給 useRef,它用於初始化可變 ref 物件公開的當前屬性。當我們使用useRef時,需要給其指定型別: typescript const nameInput = React.useRef<HTMLInputElement>(null) 這裡給例項的型別指定為了input輸入框型別。 ​

當useRef的初始值為null時,有兩種建立的形式,第一種: typescript const nameInput = React.useRef<HTMLInputElement>(null) nameInput.current.innerText = "hello world"; 這種形式下,ref1.current是隻讀的(read-only),所以當我們將它的innerText屬性重新賦值時會報以下錯誤: typescript Cannot assign to 'current' because it is a read-only property. 那該怎麼將current屬性變為動態可變得的,先來看看型別宣告檔案中 useRef 是如何定義的: typescript function useRef<T>(initialValue: T): MutableRefObject<T>; // convenience overload for refs given as a ref prop as they typically start with a null value /** * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument * (`initialValue`). The returned object will persist for the full lifetime of the component. * * Note that `useRef()` is useful for more than the `ref` attribute. It’s handy for keeping any mutable * value around similar to how you’d use instance fields in classes. * * Usage note: if you need the result of useRef to be directly mutable, include `| null` in the type * of the generic argument. * * @version 16.8.0 * @see https://reactjs.org/docs/hooks-reference.html#useref */ 這段程式碼的第十行的告訴我們,如果需要useRef的直接可變,就需要在泛型引數中包含'| null',所以這就是當初始值為null的第二種定義形式: typescript const nameInput = React.useRef<HTMLInputElement | null>(null); 這種形式下,nameInput.current就是可寫的。不過兩種型別在使用時都需要做型別檢查: typescript nameInput.current?.innerText = "hello world"; 那麼問題來了,為什麼第一種寫法在沒有操作current時沒有報錯呢?因為useRef在型別定義式具有多個過載宣告,第一種方式就是執行的以下函式過載: typescript function useRef<T>(initialValue: T|null): RefObject<T>; // convenience overload for potentially undefined initialValue / call with 0 arguments // has a default to stop it from defaulting to {} instead /** * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument * (`initialValue`). The returned object will persist for the full lifetime of the component. * * Note that `useRef()` is useful for more than the `ref` attribute. It’s handy for keeping any mutable * value around similar to how you’d use instance fields in classes. * * @version 16.8.0 * @see https://reactjs.org/docs/hooks-reference.html#useref */ 從上useRef的宣告中可以看到,function useRef的返回值型別化是MutableRefObject,這裡面的T就是引數的型別T,所以最終nameInput 的型別就是React.MutableRefObject。 ​

注意,上面用到了HTMLInputElement型別,這是一個標籤型別,這個操作就是用來訪問DOM元素的。

4. useCallback

先來看看型別宣告檔案中對useCallback的定義: typescript function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T; /** * `useMemo` will only recompute the memoized value when one of the `deps` has changed. * * Usage note: if calling `useMemo` with a referentially stable function, also give it as the input in * the second argument. * *ts * function expensive () { ... } * * function Component () { * const expensiveResult = useMemo(expensive, [expensive]) * return ... * } * * * @version 16.8.0 * @see https://reactjs.org/docs/hooks-reference.html#usememo */ useCallback接收一個回撥函式和一個依賴陣列,只有當依賴陣列中的值發生變化時才會重新執行回撥函式。來看一個例子: ```typescript const add = (a: number, b: number) => a + b;

const memoizedCallback = useCallback( (a) => { add(a, b); }, [b] );

這裡我們沒有給回撥函式中的引數a定義型別,所以下面的呼叫方式都不會報錯:typescript memoizedCallback("hello"); memoizedCallback(5) 儘管add方法的兩個引數都是number型別,但是上述呼叫都能夠用執行。所以為了更加嚴謹,我們需要給回撥函式定義具體的型別:typescript const memoizedCallback = useCallback( (a: number) => { add(a, b); }, [b] ); ``` 這時候如果再給回撥函式傳入字串就會報錯了:

image.png 所有,需要注意,在使用useCallback時需要給回撥函式的引數指定型別。

5. useMemo

先來看看型別宣告檔案中對useMemo的定義: typescript function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T; /** * `useDebugValue` can be used to display a label for custom hooks in React DevTools. * * NOTE: We don’t recommend adding debug values to every custom hook. * It’s most valuable for custom hooks that are part of shared libraries. * * @version 16.8.0 * @see https://reactjs.org/docs/hooks-reference.html#usedebugvalue */ useMemo和useCallback是非常類似的,但是它返回的是一個值,而不是函式。所以在定義useMemo時需要定義返回值的型別: ```typescript let a = 1; setTimeout(() => { a += 1; }, 1000);

const calculatedValue = useMemo(() => a ** 2, [a]); 如果返回值不一致,就會報錯:typescript const calculatedValue = useMemo(() => a + "hello", [a]); // 型別“() => string”的引數不能賦給型別“() => number”的引數 ```

6. useContext

useContext需要提供一個上下文物件,並返回所提供的上下文的值,當提供者更新上下文物件時,引用這些上下文物件的元件就會重新渲染: ```typescript const ColorContext = React.createContext({ color: "green" });

const Welcome = () => { const { color } = useContext(ColorContext); return

hello world
; }; 在使用useContext時,會自動推斷出提供的上下文物件的型別,所以並不需要我們手動設定context的型別。當前,我們也可以使用泛型來設定context的型別:typescript interface IColor { color: string; }

const ColorContext = React.createContext({ color: "green" }); 下面是useContext在型別宣告檔案中的定義:typescript function useContext(context: Context/, (not public API) observedBits?: number|boolean /): T; /* * Returns a stateful value, and a function to update it. * * @version 16.8.0 * @see https://reactjs.org/docs/hooks-reference.html#usestate / ```

7. useReducer

有時我們需要處理一些複雜的狀態,並且可能取決於之前的狀態。這時候就可以使用useReducer,它接收一個函式,這個函式會根據之前的狀態來計算一個新的state。其語法如下: typescript const [state, dispatch] = useReducer(reducer, initialArg, init); 來看下面的例子: ```typescript const reducer = (state, action) => { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); } }

const Counter = () => { const initialState = {count: 0} const [state, dispatch] = useReducer(reducer, initialState);

return ( <> Count: {state.count}
); } 當前的狀態是無法推斷出來的,可以給reducer函式新增型別,通過給reducer函式定義state和action來推斷 useReducer 的型別,下面來修改上面的例子:typescript type ActionType = { type: 'increment' | 'decrement'; };

type State = { count: number };

const initialState: State = {count: 0} const reducer = (state: State, action: ActionType) => { // ... } 這樣,在Counter函式中就可以推斷出型別。當我們檢視使用一個不存在的型別時,就會報錯:typescript dispatch({type: 'reset'}); // Error! type '"reset"' is not assignable to type '"increment" | "decrement"' 除此之外,還可以使用泛型的形式來實現reducer函式的型別定義:typescript type ActionType = { type: 'increment' | 'decrement'; };

type State = { count: number };

const reducer: React.Reducer = (state, action) => { // ... } ``` 其實dispatch方法也是有型別的:

image.png

可以看到,dispatch的型別是:React.Dispatch,上面示例的完整程式碼如下: ```typescript import React, { useReducer } from "react";

type ActionType = { type: "increment" | "decrement"; };

type State = { count: number };

const Counter: React.FC = () => { const reducer: React.Reducer = (state, action) => { switch (action.type) { case "increment": return { count: state.count + 1 }; case "decrement": return { count: state.count - 1 }; default: throw new Error(); } };

const initialState: State = {count: 0} const [state, dispatch] = useReducer(reducer, initialState);

return ( <> Count: {state.count}
); };

export default Counter;

```

四、事件處理

1. Event 事件型別

在開發中我們會經常在事件處理函式中使用event事件物件,比如在input框輸入時實時獲取輸入的值;使用滑鼠事件時,通過 clientX、clientY 獲取當前指標的座標等等。 ​

我們知道,Event是一個物件,並且有很多屬性,這時很多人就會把 event 型別定義為any,這樣的話TypeScript就失去了它的意義,並不會對event事件進行靜態檢查,如果一個鍵盤事件觸發了下面的方法,也不會報錯: typescript const handleEvent = (e: any) => { console.log(e.clientX, e.clientY) } 由於Event事件物件中有很多的屬性,所以我們也不方便把所有屬性及其型別定義在一個interface中,所以React在宣告檔案中給我們提供了Event事件物件的型別宣告。 ​

常見的Event 事件物件如下:

  • 剪下板事件物件:ClipboardEvent
  • 拖拽事件物件:DragEvent
  • 焦點事件物件:FocusEvent
  • 表單事件物件:FormEvent
  • Change事件物件:ChangeEvent
  • 鍵盤事件物件:KeyboardEvent
  • 滑鼠事件物件:MouseEvent
  • 觸控事件物件:TouchEvent
  • 滾輪事件物件:WheelEvent
  • 動畫事件物件:AnimationEvent
  • 過渡事件物件:TransitionEvent

可以看到,這些Event事件物件的泛型中都會接收一個Element元素的型別,這個型別就是我們繫結這個事件的標籤元素的型別,標籤元素型別將在下面的第五部分介紹。

來看一個簡單的例子: ```typescript type State = { text: string; };

const App: React.FC = () => {
const [text, setText] = useState("")

const onChange = (e: React.FormEvent): void => { setText(e.currentTarget.value); };

return (

); }

``` 這裡就給onChange方法的事件物件定義為了FormEvent型別,並且作用的物件時一個HTMLInputElement型別的標籤(input標籤) ​

可以來看下MouseEvent事件物件和ChangeEvent事件物件的型別宣告,其他事件物件的宣告形似也類似: ```typescript interface MouseEvent extends UIEvent { altKey: boolean; button: number; buttons: number; clientX: number; clientY: number; ctrlKey: boolean; /* * See DOM Level 3 Events spec. for a list of valid (case-sensitive) arguments to this method. / getModifierState(key: string): boolean; metaKey: boolean; movementX: number; movementY: number; pageX: number; pageY: number; relatedTarget: EventTarget | null; screenX: number; screenY: number; shiftKey: boolean; }

interface ChangeEvent extends SyntheticEvent { target: EventTarget & T; } 在很多事件物件的宣告檔案中都可以看到 EventTarget 的身影。這是因為,DOM的事件操作(監聽和觸發),都定義在EventTarget介面上。EventTarget 的型別宣告如下:typescript

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; } 比如在change事件中,會使用的e.target來獲取當前的值,它的的型別就是EventTarget。來看下面的例子:typescript onSourceChange(e)} placeholder="最多30個字" />

const onSourceChange = (e: React.ChangeEvent) => { if (e.target.value.length > 30) { message.error('請長度不能超過30個字,請重新輸入'); return; } setSourceInput(e.target.value); }; ``` 這裡定義了一個input輸入框,當觸發onChange事件時,會呼叫onSourceChange方法,該方法的引數e的型別就是:React.ChangeEvent,而e.target的型別就是EventTarget:

image.png

在來看一個例子: ```typescript questionList.map(item => (

handleChangeCurrent(item, e)} > // 元件內容...
)

const handleChangeCurrent = (item: IData, e: React.MouseEvent) => { e.stopPropagation(); setCurrent(item); }; 這點程式碼中,點選某個盒子,就將它設定為當前的盒子,方便執行其他操作。當滑鼠點選盒子時,會觸發handleChangeCurren方法,該方法有兩個引數,第二個引數是event物件,在方法中執行了e.stopPropagation();是為了阻止冒泡事件,這裡的stopPropagation()實際上並不是滑鼠事件MouseEvent的屬性,它是合成事件上的屬性,來看看宣告檔案中的定義:typescript interface MouseEvent extends UIEvent { //...
}

interface UIEvent extends SyntheticEvent { //... }

interface SyntheticEvent extends BaseSyntheticEvent {}

interface BaseSyntheticEvent { nativeEvent: E; currentTarget: C; target: T; bubbles: boolean; cancelable: boolean; defaultPrevented: boolean; eventPhase: number; isTrusted: boolean; preventDefault(): void; isDefaultPrevented(): boolean; stopPropagation(): void; isPropagationStopped(): boolean; persist(): void; timeStamp: number; type: string; } 可以看到,這裡的stopPropagation()是一層層的繼承來的,最終來自於BaseSyntheticEvent合成事件型別。原生的事件集合SyntheticEvent就是繼承自合成時間型別。SyntheticEvent<T = Element, E = Event>泛型介面接收當前的元素型別和事件型別,如果不介意這兩個引數的型別,完全可以這樣寫:typescript )=>{ //... }} /> ```

2. 事件處理函式型別

說完事件物件型別,再來看看事件處理函式的型別。React也為我們提供了貼心的提供了事件處理函式的型別宣告,來看看所有的事件處理函式的型別宣告: ```typescript type EventHandler> = { bivarianceHack(event: E): void }["bivarianceHack"];

type ReactEventHandler = EventHandler>; // 剪下板事件處理函式 type ClipboardEventHandler = EventHandler>; // 複合事件處理函式 type CompositionEventHandler = EventHandler>; // 拖拽事件處理函式 type DragEventHandler = EventHandler>; // 焦點事件處理函式 type FocusEventHandler = EventHandler>; // 表單事件處理函式 type FormEventHandler = EventHandler>; // Change事件處理函式 type ChangeEventHandler = EventHandler>; // 鍵盤事件處理函式 type KeyboardEventHandler = EventHandler>; // 滑鼠事件處理函式 type MouseEventHandler = EventHandler>; // 觸屏事件處理函式 type TouchEventHandler = EventHandler>; // 指標事件處理函式 type PointerEventHandler = EventHandler>; // 介面事件處理函式 type UIEventHandler = EventHandler>; // 滾輪事件處理函式 type WheelEventHandler = EventHandler>; // 動畫事件處理函式 type AnimationEventHandler = EventHandler>; // 過渡事件處理函式 type TransitionEventHandler = EventHandler>; ``` 這裡面的T的型別也都是Element,指的是觸發該事件的HTML標籤元素的型別,下面第五部分會介紹。 ​

EventHandler會接收一個E,它表示事件處理函式中 Event 物件的型別。bivarianceHack 是事件處理函式的型別定義,函式接收一個 Event 物件,並且其型別為接收到的泛型變數 E 的型別, 返回值為 void。 ​

還看上面的那個例子: ```typescript type State = { text: string; };

const App: React.FC = () => {
const [text, setText] = useState("")

const onChange: React.ChangeEventHandler = (e) => { setText(e.currentTarget.value); };

return (

); }

``` 這裡給onChange方法定義了方法的型別,它是一個ChangeEventHandler的型別,並且作用的物件時一個HTMLImnputElement型別的標籤(input標籤)。

五、HTML標籤型別

1. 常見標籤型別

在專案的依賴檔案中可以找到HTML標籤相關的型別宣告檔案:

image.png

所有的HTML標籤的型別都被定義在 intrinsicElements 介面中,常見的標籤及其型別如下: typescript a: HTMLAnchorElement; body: HTMLBodyElement; br: HTMLBRElement; button: HTMLButtonElement; div: HTMLDivElement; h1: HTMLHeadingElement; h2: HTMLHeadingElement; h3: HTMLHeadingElement; html: HTMLHtmlElement; img: HTMLImageElement; input: HTMLInputElement; ul: HTMLUListElement; li: HTMLLIElement; link: HTMLLinkElement; p: HTMLParagraphElement; span: HTMLSpanElement; style: HTMLStyleElement; table: HTMLTableElement; tbody: HTMLTableSectionElement; video: HTMLVideoElement; audio: HTMLAudioElement; meta: HTMLMetaElement; form: HTMLFormElement; 那什麼時候會使用到標籤型別呢,上面第四部分的Event事件型別和事件處理函式型別中都使用到了標籤的型別。上面的很多的型別都需要傳入一個ELement型別的泛型引數,這個泛型引數就是對應的標籤型別值,可以根據標籤來選擇對應的標籤型別。這些型別都繼承自HTMLElement型別,如果使用時對型別型別要求不高,可以直接寫HTMLELement。比如下面的例子: ```typescript ``` 其實,在直接操作DOM時也會用到標籤型別,雖然我們現在通常會使用框架來開發,但是有時候也避免不了直接操作DOM。比如我在工作中,專案中的某一部分元件是通過npm來引入的其他組的元件,而在很多時候,我有需要動態的去個性化這個元件的樣式,最直接的辦法就是通過原生JavaScript獲取到DOM元素,來進行樣式的修改,這時候就會用到標籤型別。 ​

來看下面的例子: ```typescript document.querySelectorAll('.paper').forEach(item => { const firstPageHasAddEle = (item.firstChild as HTMLDivElement).classList.contains('add-ele');

if (firstPageHasAddEle) { item.removeChild(item.firstChild as ChildNode); } }) ``` 這是我最近寫的一段程式碼(略微刪改),在第一頁有個add-ele元素的時候就刪除它。這裡我們將item.firstChild斷言成了HTMLDivElement型別,如果不斷言,item.firstChild的型別就是ChildNode,而ChildNode型別中是不存在classList屬性的,所以就就會報錯,當我們把他斷言成HTMLDivElement型別時,就不會報錯了。很多時候,標籤型別可以和斷言(as)一起使用。 ​

後面在removeChild時又使用了as斷言,為什麼呢?item.firstChild不是已經自動識別為ChildNode型別了嗎?因為TS會認為,我們可能不能獲取到類名為paper的元素,所以item.firstChild的型別就被推斷為ChildNode | null,我們有時候比TS更懂我們定義的元素,知道頁面一定存在paper 元素,所以可以直接將item.firstChild斷言成ChildNode型別。

2. 標籤屬性型別

眾所周知,每個HTML標籤都有自己的屬性,比如Input框就有value、width、placeholder、max-length等屬性,下面是Input框的屬性型別定義: ```typescript interface InputHTMLAttributes extends HTMLAttributes { accept?: string | undefined; alt?: string | undefined; autoComplete?: string | undefined; autoFocus?: boolean | undefined; capture?: boolean | string | undefined; checked?: boolean | undefined; crossOrigin?: string | undefined; disabled?: boolean | undefined; enterKeyHint?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send' | undefined; form?: string | undefined; formAction?: string | undefined; formEncType?: string | undefined; formMethod?: string | undefined; formNoValidate?: boolean | undefined; formTarget?: string | undefined; height?: number | string | undefined; list?: string | undefined; max?: number | string | undefined; maxLength?: number | undefined; min?: number | string | undefined; minLength?: number | undefined; multiple?: boolean | undefined; name?: string | undefined; pattern?: string | undefined; placeholder?: string | undefined; readOnly?: boolean | undefined; required?: boolean | undefined; size?: number | undefined; src?: string | undefined; step?: number | string | undefined; type?: string | undefined; value?: string | ReadonlyArray | number | undefined; width?: number | string | undefined;

onChange?: ChangeEventHandler | undefined; } ``` 如果我們需要直接操作DOM,就可能會用到元素屬性型別,常見的元素屬性型別如下:

  • HTML屬性型別:HTMLAttributes
  • 按鈕屬性型別:ButtonHTMLAttributes
  • 表單屬性型別:FormHTMLAttributes
  • 圖片屬性型別:ImgHTMLAttributes
  • 輸入框屬性型別:InputHTMLAttributes
  • 連結屬性型別:LinkHTMLAttributes
  • meta屬性型別:MetaHTMLAttributes
  • 選擇框屬性型別:SelectHTMLAttributes
  • 表格屬性型別:TableHTMLAttributes
  • 輸入區屬性型別:TextareaHTMLAttributes
  • 影片屬性型別:VideoHTMLAttributes
  • SVG屬性型別:SVGAttributes
  • WebView屬性型別:WebViewHTMLAttributes

一般情況下,我們是很少需要在專案中顯式的去定義標籤屬性的型別。如果子級去封裝元件庫的話,這些屬性就能發揮它們的作用了。來看例子(來源於網路,僅供學習): ```typescript import React from 'react'; import classNames from 'classnames'

export enum ButtonSize {     Large = 'lg',     Small = 'sm' }

export enum ButtonType {     Primary = 'primary',     Default = 'default',     Danger = 'danger',     Link = 'link' }

interface BaseButtonProps {     className?: string;     disabled?: boolean;     size?: ButtonSize;     btnType?: ButtonType;     children: React.ReactNode;     href?: string;     }

type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes // 使用 交叉型別(&) 獲得我們自己定義的屬性和原生 button 的屬性 type AnchorButtonProps = BaseButtonProps & React.AnchorHTMLAttributes // 使用 交叉型別(&) 獲得我們自己定義的屬性和原生 a標籤 的屬性

export type ButtonProps = Partial //使用 Partial<> 使兩種屬性可選

const Button: React.FC = (props) => {     const {          disabled,         className,          size,         btnType,         children,         href,         ...restProps       } = props;

const classes = classNames('btn', className, {         [btn-${btnType}]: btnType,         [btn-${size}]: size,         'disabled': (btnType === ButtonType.Link) && disabled  // 只有 a 標籤才有 disabled 類名,button沒有     })

if(btnType === ButtonType.Link && href) {         return (                              {children}                      )

} else {         return (             <button               className={classes}              disabled={disabled} // button元素預設有disabled屬性,所以即便沒給他設定樣式也會和普通button有一定區別

{...restProps}             >                 {children}                      )     } }

Button.defaultProps = {     disabled: false,     btnType: ButtonType.Default }

export default Button; 這段程式碼就是用來封裝一個buttom按鈕,在button的基礎上添加了一些自定義屬性,比如上面將button的型別使用交叉型別(&)獲得自定義屬性和原生 button 屬性 :typescript type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes  ``` 可以看到,標籤屬性型別在封裝元件庫時還是很有用的,更多用途可以自己探索~

六、工具泛型

在專案中使用一些工具泛型可以提高我們的開發效率,少寫很多型別定義。下面來看看有哪些常見的工具泛型,以及其使用方式。

1. Partial

Partial 作用是將傳入的屬性變為可選項。適用於對型別結構不明確的情況。它使用了兩個關鍵字:keyof和in,先來看看他們都是什麼含義。keyof 可以用來取得介面的所有 key 值: typescript interface Person { name: string; age: number; height: number; } type T = keyof Foo // T 型別為: "name" | "age" | "number" in關鍵字可以遍歷列舉型別,: ```typescript type Person = "name" | "age" | "number" type Obj = {

} // Obj型別為: { name: any, age: any, number: any } keyof 可以產生聯合型別, in 可以遍歷列舉型別, 所以經常一起使用, 下面是Partial工具泛型的定義:typescript /* * Make all properties in T optional * 將T中的所有屬性設定為可選 / type Partial = { [P in keyof T]?: T[P]; }; ``` 這裡,keyof T 獲取 T 所有屬性名, 然後使用 in 進行遍歷, 將值賦給 P, 最後 T[P] 取得相應屬性的值。中間的?就用來將屬性設定為可選。 ​

使用示例如下: ```typescript interface IPerson { name: string; age: number; height: number; }

const person: Partial = { name: "zhangsan"; } ```

2. Required

Required 的作用是將傳入的屬性變為必選項,和上面的工具泛型恰好相反,其宣告如下: typescript /** * Make all properties in T required * 將T中的所有屬性設定為必選 */ type Required<T> = { [P in keyof T]-?: T[P]; }; 可以看到,這裡使用-?將屬性設定為必選,可以理解為減去問號。適用形式和上面的Partial差不多: ```typescript interface IPerson { name?: string; age?: number; height?: number; }

const person: Required = { name: "zhangsan"; age: 18; height: 180; } ```

3. Readonly

將T型別的所有屬性設定為只讀(readonly),構造出來型別的屬性不能被再次賦值。Readonly的宣告形式如下: typescript /** * Make all properties in T readonly */ type Readonly<T> = { readonly [P in keyof T]: T[P]; }; 使用示例如下: ```typescript interface IPerson { name: string; age: number; }

const person: Readonly = { name: "zhangsan", age: 18 }

person.age = 20; // Error: cannot reassign a readonly property ``` 可以看到,通過 Readonly 將IPerson的屬性轉化成了只讀,不能再進行賦值操作。

4. Pick

從T型別中挑選部分屬性K來構造新的型別。它的宣告形式如下: typescript /** * From T, pick a set of properties whose keys are in the union K */ type Pick<T, K extends keyof T> = { [P in K]: T[P]; }; 使用示例如下: ```typescript interface IPerson { name: string; age: number; height: number; }

const person: Pick = { name: "zhangsan", age: 18 } ```

5. Record

Record 用來構造一個型別,其屬性名的型別為K,屬性值的型別為T。這個工具泛型可用來將某個型別的屬性對映到另一個型別上,下面是其宣告形式: typescript /** * Construct a type with a set of properties K of type T */ type Record<K extends keyof any, T> = { [P in K]: T; }; 使用示例如下: ```typescript interface IPageinfo { name: string; }

type IPage = 'home' | 'about' | 'contact';

const page: Record = { about: {title: 'about'}, contact: {title: 'contact'}, home: {title: 'home'}, } ```

6. Exclude

Exclude 就是從一個聯合型別中排除掉屬於另一個聯合型別的子集,下面是其宣告的形式: typescript /** * Exclude from T those types that are assignable to U */ type Exclude<T, U> = T extends U ? never : T; 使用示例如下: ```typescript interface IPerson { name: string; age: number; height: number; }

const person: Exclude = { name: "zhangsan"; height: 180; } ```

7. Omit

上面的Pick 和 Exclude 都是最基礎基礎的工具泛型,很多時候用 Pick 或者 Exclude 還不如直接寫型別更直接。而 Omit 就基於這兩個來做的一個更抽象的封裝,它允許從一個物件中剔除若干個屬性,剩下的就是需要的新型別。下面是它的宣告形式: typescript /** * Construct a type with the properties of T except for those in type K. */ type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; 使用示例如下: ```typescript interface IPerson { name: string; age: number; height: number; }

const person: Omit = { name: "zhangsan"; } ```

8. ReturnType

ReturnType會返回函式返回值的型別,其宣告形式如下: typescript /** * Obtain the return type of a function type */ type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any; 使用示例如下: ```typescript function foo(type): boolean { return type === 0 }

type FooType = ReturnType ``` 這裡使用 typeof 是為了獲取 foo 的函式簽名,等價於 (type: any) => boolean。

七、Axios 封裝

在React專案中,我們經常使用Axios庫進行資料請求,Axios 是基於 Promise 的 HTTP 庫,可以在瀏覽器和 node.js 中使用。Axios 具備以下特性:

  • 從瀏覽器中建立 XMLHttpRequests;
  • 從 node.js 建立 HTTP 請求;
  • 支援 Promise API;
  • 攔截請求和響應;
  • 轉換請求資料和響應資料;
  • 取消請求;
  • 自動轉換 JSON 資料;
  • 客戶端支援防禦 XSRF。

Axios的基本使用就不再多介紹了。為了更好地呼叫,做一些全域性的攔截,通常會對Axios進行封裝,下面就使用TypeScript對Axios進行簡單封裝,使其同時能夠有很好的型別支援。Axios是自帶宣告檔案的,所以我們無需額外的操作。 ​

下面來看基本的封裝: ```typescript import axios, { AxiosInstance, AxiosRequestConfig, AxiosPromise,AxiosResponse } from 'axios'; // 引入axios和定義在node_modules/axios/index.ts檔案裡的型別宣告

// 定義介面請求類,用於建立axios請求例項 class HttpRequest { // 接收介面請求的基本路徑 constructor(public baseUrl: string) { this.baseUrl = baseUrl; }

// 呼叫介面時呼叫例項的這個方法,返回AxiosPromise public request(options: AxiosRequestConfig): AxiosPromise { // 建立axios例項,它是函式,同時這個函式包含多個屬性 const instance: AxiosInstance = axios.create() // 合併基礎路徑和每個介面單獨傳入的配置,比如url、引數等 options = this.mergeConfig(options) // 呼叫interceptors方法使攔截器生效 this.interceptors(instance, options.url) // 返回AxiosPromise return instance(options) }

// 用於新增全域性請求和響應攔截 private interceptors(instance: AxiosInstance, url?: string) { // 請求和響應攔截 }

// 用於合併基礎路徑配置和介面單獨配置 private mergeConfig(options: AxiosRequestConfig): AxiosRequestConfig { return Object.assign({ baseURL: this.baseUrl }, options); } } export default HttpRequest; 通常baseUrl在開發環境的和生產環境的路徑是不一樣的,所以可以根據當前是開發環境還是生產環境做判斷,應用不同的基礎路徑。這裡要寫在一個配置檔案裡:typescript export default { api: { devApiBaseUrl: '/test/api/xxx', proApiBaseUrl: '/api/xxx', }, }; 在上面的檔案中引入這個配置:typescript import { api: { devApiBaseUrl, proApiBaseUrl } } from '@/config'; const apiBaseUrl = env.NODE_ENV === 'production' ? proApiBaseUrl : devApiBaseUrl; 之後就可以將apiBaseUrl作為預設值傳入HttpRequest的引數:typescript class HttpRequest { constructor(public baseUrl: string = apiBaseUrl) { this.baseUrl = baseUrl; } 接下來可以完善一下攔截器類,在類中interceptors方法內新增請求攔截器和響應攔截器,實現對所有介面請求的統一處理:typescript private interceptors(instance: AxiosInstance, url?: string) { // 請求攔截 instance.interceptors.request.use((config: AxiosRequestConfig) => { // 介面請求的所有配置,可以在axios.defaults修改配置 return config }, (error) => { return Promise.reject(error) })

// 響應攔截
instance.interceptors.response.use((res: AxiosResponse) => {
  const { data } = res 
  const { code, msg } = data
  if (code !== 0) {
    console.error(msg) 
  }
  return res
},
(error) => { 
  return Promise.reject(error)
})

} 到這裡封裝的就差不多了,一般服務端會將狀態碼、提示資訊和資料封裝在一起,然後作為資料返回,所以所有請求返回的資料格式都是一樣的,所以就可以定義一個介面來指定返回的資料結構,可以定義一個介面:typescript export interface ResponseData { code: number data?: any msg: string } 接下來看看使用TypeScript封裝的Axios該如何使用。可以先定義一個請求例項:typescript import HttpRequest from '@/utils/axios' export * from '@/utils/axios' export default new HttpRequest() 這裡把請求類匯入進來,預設匯出這個類的例項。之後建立一個登陸介面請求方法:typescript import axios, { ResponseData } from './index' import { AxiosPromise } from 'axios'

interface ILogin { user: string; password: number | string }

export const loginReq = (data: ILogin): AxiosPromise => { return axios.request({ url: '/api/user/login', data, method: 'POST' }) } `` 這裡封裝登入請求方法loginReq,他的引數必須是我們定義的ILogin介面的型別。這個方法返回一個型別為AxiosPromise`的Promise,AxiosPromise是axios宣告檔案內建的型別,可以傳入一個泛型變數引數,用於指定返回的結果中data欄位的型別。 ​

接下來可以呼叫一下這個登入的介面: ```typescript import { loginReq } from '@/api/user'

const Home: FC = () => { const login = (params) => { loginReq(params).then((res) => { console.log(res.data.code) })
}
} ``` 通過這種方式,當我們呼叫loginReq介面時,就會提示我們,引數的型別是ILogin,需要傳入幾個引數。這樣編寫程式碼的體驗就會好很多。

八. 其他

1. import React

在React專案中使用TypeScript時,普通元件檔案字尾為.tsx,公共方法檔案字尾為.ts。在. tsx 檔案中匯入 React 的方式如下: typescript import * as React from 'react' import * as ReactDOM from 'react-dom' 這是一種面向未來的匯入方式,如果想在專案中使用以下匯入方式: typescript import React from "react"; import ReactDOM from "react-dom"; 就需要在tsconfig.json配置檔案中進行如下配置: typescript "compilerOptions": { // 允許預設從沒有預設匯出的模組匯入。 "allowSyntheticDefaultImports": true, }

2. Types or Interfaces?

我們可以使用types或者Interfaces來定義型別嗎,那麼該如何選擇他倆呢?建議如下:

  • 在定義公共 API 時(比如編輯一個庫)使用 interface,這樣可以方便使用者繼承介面,這樣允許使用最通過宣告合併來擴充套件它們;
  • 在定義元件屬性(Props)和狀態(State)時,建議使用 type,因為 type 的約束性更強。

interface 和 type 在 ts 中是兩個不同的概念,但在 React 大部分使用的 case 中,interface 和 type 可以達到相同的功能效果,type 和 interface 最大的區別是:type 型別不能二次編輯,而 interface 可以隨時擴充套件: ```typescript interface Animal { name: string }

// 可以繼續在原屬性基礎上,新增新屬性:color interface Animal { color: string }

type Animal = { name: string } // type型別不支援屬性擴充套件 // Error: Duplicate identifier 'Animal' type Animal = { color: string } ``` type對於聯合型別是很有用的,比如:type Type = TypeA | TypeB。而interface更適合宣告字典類行,然後定義或者擴充套件它。

3. 懶載入型別

如果我們想在React router中使用懶載入,React也為我們提供了懶載入方法的型別,來看下面的例子: ```typescript export interface RouteType { pathname: string; component: LazyExoticComponent; exact: boolean; title?: string; icon?: string; children?: RouteType[]; } export const AppRoutes: RouteType[] = [ { pathname: '/login', component: lazy(() => import('../views/Login/Login')), exact: true }, { pathname: '/404', component: lazy(() => import('../views/404/404')), exact: true, }, { pathname: '/', exact: false, component: lazy(() => import('../views/Admin/Admin')) } ]

下面是懶載入型別和lazy方法在宣告檔案中的定義:typescript type LazyExoticComponent> = ExoticComponent> & { readonly _result: T; };

function lazy>( factory: () => Promise<{ default: T }> ): LazyExoticComponent; ```

4. 型別斷言

型別斷言(Type Assertion)可以用來手動指定一個值的型別。在React專案中,斷言還是很有用的,。有時候推斷出來的型別並不是真正的型別,很多時候我們可能會比TS更懂我們的程式碼,所以可以使用斷言(使用as關鍵字)來定義一個值得型別。 ​

來看下面的例子: typescript const getLength = (target: string | number): number => { if (target.length) { // error 型別"string | number"上不存在屬性"length" return target.length; // error 型別"number"上不存在屬性"length" } else { return target.toString().length; } }; 當TypeScript不確定一個聯合型別的變數到底是哪個型別時,就只能訪問此聯合型別的所有型別裡共有的屬性或方法,所以現在加了對引數target和返回值的型別定義之後就會報錯。這時就可以使用斷言,將target的型別斷言成string型別: typescript const getStrLength = (target: string | number): number => { if ((target as string).length) { return (target as string).length; } else { return target.toString().length; } }; 需要注意,型別斷言並不是型別轉換,斷言成一個聯合型別中不存在的型別是不允許的。 ​

再來看一個例子,在呼叫一個方法時傳入引數: image.png 這裡就提示我們這個引數可能是undefined,而通過業務知道這個值是一定存在的,所以就可以將它斷言成數字:data?.subjectId as number

除此之外,上面所說的標籤型別、元件型別、時間型別都可以使用斷言來指定給一些資料,還是要根據實際的業務場景來使用。 ​

感悟:使用型別斷言真的能解決專案中的很多報錯~

5. 列舉型別

列舉型別在專案中的作用也是不可忽視的,使用列舉型別可以讓程式碼的擴充套件性更好,當我想更改某屬性值時,無需去全域性更改這個屬性,只要更改列舉中的值即可。通常情況下,最好新建一個檔案專門來定義列舉值,便於引用。關於列舉型別的語法這裡不在多介紹,可以參考之前的文章:《TS入門篇 | 詳解 TypeScript 列舉型別》

關於在React專案中如何優雅的使用TypeScript就先介紹這麼多,後面有新的內容會再分享給大家。如果覺得不錯就點個贊吧!

三連 .gif