<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 34344字,需瀏覽 69分鐘

           ·

          2021-11-09 22:13

          大廠技術(shù)??高級(jí)前端??Node進(jìn)階

          點(diǎn)擊上方?程序員成長(zhǎng)指北,關(guān)注公眾號(hào)

          回復(fù)1,加入高級(jí)Node交流群

          工作用的技術(shù)棧主要是React hooks + TypeScript。使用三月有余,其實(shí)在單獨(dú)使用 TypeScript 時(shí)沒(méi)有太多的坑,不過(guò)和React結(jié)合之后就會(huì)復(fù)雜很多。本文就來(lái)聊一聊TypeScript與React一起使用時(shí)經(jīng)常遇到的一些類型定義的問(wèn)題。閱讀本文前,希望你能有一定的React和TypeScript基礎(chǔ)。

          一、組件聲明

          在React中,組件的聲明方式有兩種:函數(shù)組件類組件, 來(lái)看看這兩種類型的組件聲明時(shí)是如何定義TS類型的。

          1. 類組件

          類組件的定義形式有兩種:React.ComponentReact.PureComponent,它們都是泛型接口,接收兩個(gè)參數(shù),第一個(gè)是props類型的定義,第二個(gè)是state類型的定義,這兩個(gè)參數(shù)都不是必須的,沒(méi)有時(shí)可以省略:

          interface?IProps?{
          ??name:?string;
          }

          interface?IState?{
          ??count:?number;
          }

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

          ??render()?{
          ????return?(
          ??????

          ????????{this.state.count}
          ????????{this.props.name}
          ??????</div>
          ????);
          ??}
          }

          export?default?App;

          React.PureComponent 也是差不多的:

          class?App?extends?React.PureComponent?{}

          React.PureComponent是有第三個(gè)參數(shù)的,它表示getSnapshotBeforeUpdate的返回值。

          那PureComponent和Component 的區(qū)別是什么呢?它們的主要區(qū)別是PureComponent中的shouldComponentUpdate 是由自身進(jìn)行處理的,不需要我們自己處理,所以PureComponent可以在一定程度上提升性能。

          有時(shí)候可能會(huì)見(jiàn)到這種寫(xiě)法,實(shí)際上和上面的效果是一樣的:

          import?React,?{PureComponent,?Component}?from?"react";

          class?App?extends?PureComponent?{}

          class?App?extends?Component?{}

          那如果定義時(shí)候我們不知道組件的props的類型,只有在調(diào)用時(shí)才知道組件類型,該怎么辦呢?這時(shí)泛型就發(fā)揮作用了:

          //?定義組件
          class?MyComponent

          ?extends?React.Component

          ?{
          ??internalProp:?P;
          ??constructor(props:?P)?{
          ????super(props);
          ????this.internalProp?=?props;
          ??}
          ??render()?{
          ????return?(
          ??????hello?world</span>
          ????);
          ??}
          }

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

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

          2. 函數(shù)組件

          通常情況下,函數(shù)組件我是這樣寫(xiě)的:

          interface?IProps?{
          ??name:?string
          }

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

          ??return?(
          ????"App">
          ??????

          hello?world</h1>
          ??????

          {name}h2>
          ????</div>
          ??);
          }

          export?default?App;

          除此之外,函數(shù)類型還可以使用React.FunctionComponent來(lái)定義,也可以使用其簡(jiǎn)寫(xiě)React.FC,兩者效果是一樣的。它是一個(gè)泛型接口,可以接收一個(gè)參數(shù),參數(shù)表示props的類型,這個(gè)參數(shù)不是必須的。它們就相當(dāng)于這樣:

          type?React.FC?=?React.FunctionComponent


          最終的定義形式如下:

          interface?IProps?{
          ??name:?string
          }

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

          hello?world</h1>
          ??????

          {name}h2>
          ????</div>
          ??);
          }

          export?default?App;

          當(dāng)使用這種形式來(lái)定義函數(shù)組件時(shí),props中默認(rèn)會(huì)帶有children屬性,它表示該組件在調(diào)用時(shí),其內(nèi)部的元素,來(lái)看一個(gè)例子,首先定義一個(gè)組件,組件中引入了Child1和Child2組件:

          import?Child1?from?"./child1";
          import?Child2?from?"./child2";

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

          export?default?App;

          Child1組件結(jié)構(gòu)如下:

          interface?IProps?{
          ??name:?string;
          }
          const?Child1:?React.FC?=?(props)?=>?{
          ??const?{?name,?children?}?=?props;
          ??console.log(children);
          ??return?(
          ????"App">
          ??????

          hello?child1</h1>
          ??????

          {name}h2>
          ????</div>
          ??);
          };

          export?default?Child1;

          我們?cè)贑hild1組件中打印了children屬性,它的值是一個(gè)數(shù)組,包含Child2對(duì)象和后面的文本:

          使用 React.FC 聲明函數(shù)組件和普通聲明的區(qū)別如下:

          • React.FC 顯式地定義了返回類型,其他方式是隱式推導(dǎo)的;
          • React.FC 對(duì)靜態(tài)屬性:displayName、propTypes、defaultProps 提供了類型檢查和自動(dòng)補(bǔ)全;
          • React.FC 為 children 提供了隱式的類型(ReactElement | null)。

          那如果我們?cè)诙x組件時(shí)不知道props的類型,只有調(diào)用時(shí)才知道,那就還是用泛型來(lái)定義props的類型。對(duì)于使用function定義的函數(shù)組件:

          //?定義組件
          function?MyComponent<P>(props:?P)?{
          ??return?(
          ???
          ?????{props}
          ????</span>
          ??);
          }

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

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

          如果使用箭頭函數(shù)定義的函數(shù)組件,直接這樣調(diào)用是錯(cuò)誤的:

          const?MyComponent?=?

          (props:?P)?{
          ??return?(
          ???
          ?????{props}
          ????</span>
          ??);
          }

          必須使用extends關(guān)鍵字來(lái)定義泛型參數(shù)才能被成功解析:

          const?MyComponent?=?extends?any>(props:?P)?{
          ??return?(
          ???
          ?????{props}
          ????</span>
          ??);
          }

          二、React內(nèi)置類型

          1. JSX.Element

          先來(lái)看看JSX.Element類型的聲明:

          declare?global?{
          ??namespace?JSX?{
          ????interface?Element?extends?React.ReactElement?{?}
          ??}
          }

          可以看到,JSX.Element是ReactElement的子類型,它沒(méi)有增加屬性,兩者是等價(jià)的。也就是說(shuō)兩種類型的變量可以相互賦值。

          JSX.Element 可以通過(guò)執(zhí)行 React.createElement 或是轉(zhuǎn)譯 JSX 獲得:

          const?jsx?=?
          hello</div>
          const?ele?=?React.createElement("div",?null,?"hello");

          2. React.ReactElement

          React 的類型聲明文件中提供了 React.ReactElement<T>,它可以讓我們通過(guò)傳入<T/>來(lái)注解類組件的實(shí)例化,它在聲明文件中的定義如下:

          interface?ReactElementextends?string?|?JSXElementConstructor?=?string?|?JSXElementConstructor>?{
          ???type:?T;
          ???props:?P;
          ???key:?Key?|?null;
          }

          ReactElement是一個(gè)接口,包含type,props,key三個(gè)屬性值。該類型的變量值只能是兩種:null 和 ReactElement實(shí)例。

          通常情況下,函數(shù)組件返回ReactElement(JXS.Element)的值。

          3. React.ReactNode

          ReactNode類型的聲明如下:

          type?ReactText?=?string?|?number;
          type?ReactChild?=?ReactElement?|?ReactText;

          interface?ReactNodeArray?extends?Array?{}
          type?ReactFragment?=?{}?|?ReactNodeArray;
          type?ReactNode?=?ReactChild?|?ReactFragment?|?ReactPortal?|?boolean?|?null?|?undefined;

          可以看到,ReactNode是一個(gè)聯(lián)合類型,它可以是string、number、ReactElement、null、boolean、ReactNodeArray。由此可知。ReactElement類型的變量可以直接賦值給ReactNode類型的變量,但反過(guò)來(lái)是不行的。

          類組件的 render 成員函數(shù)會(huì)返回 ReactNode 類型的值:

          class?MyComponent?extends?React.Component?{
          ?render()?{
          ?????return?
          hello?world</div>
          ????}
          }
          /
          /?正確
          const?component:?React.ReactNode?=?>;
          //?錯(cuò)誤
          const?component:?React.ReactNode?=?;

          上面的代碼中,給component變量設(shè)置了類型是Mycomponent類型的react實(shí)例,這時(shí)只能給其賦值其為MyComponent的實(shí)例組件。

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

          4. CSSProperties

          先來(lái)看看React的聲明文件中對(duì)CSSProperties 的定義:

          export?interface?CSSProperties?extends?CSS.Properties?{
          ??/**
          ???*?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屬性類型,可以將一個(gè)方法的返回值設(shè)置為該類型:

          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?=>?(
          ??"mySidenav"?className={classNames.sidenav}?style={divStyle(props)}>
          ????{props.children}
          ??</div>
          );

          這里divStyle組件的返回值就是React.CSSProperties類型。

          我們還可以定義一個(gè)CSSProperties類型的變量:

          const?divStyle:?React.CSSProperties?=?{
          ????width:?"11rem",
          ????height:?"7rem",
          ????backgroundColor:?`rgb(${props.color.red},${props.color.green},?${props.color.blue})`
          };

          這個(gè)變量可以在HTML標(biāo)簽的style屬性上使用:


          在React的類型聲明文件中,style屬性的類型如下:

          style?:?CSSProperties?|?undefined;

          三、React Hooks

          1. useState

          默認(rèn)情況下,React會(huì)為根據(jù)設(shè)置的state的初始值來(lái)自動(dòng)推導(dǎo)state以及更新函數(shù)的類型:

          如果已知state 的類型,可以通過(guò)以下形式來(lái)自定義state的類型:

          const?[count,?setCount]?=?useState<number>(1)

          如果初始值為null,需要顯式地聲明 state 的類型:

          const?[count,?setCount]?=?useState<number?|?null>(null);?

          如果state是一個(gè)對(duì)象,想要初始化一個(gè)空對(duì)象,可以使用斷言來(lái)處理:

          const?[user,?setUser]?=?React.useState({}?as?IUser);

          實(shí)際上,這里將空對(duì)象{}斷言為IUser接口就是欺騙了TypeScript的編譯器,由于后面的代碼可能會(huì)依賴這個(gè)對(duì)象,所以應(yīng)該在使用前及時(shí)初始化 user 的值,否則就會(huì)報(bào)錯(cuò)。

          下面是聲明文件中 useState 的定義:

          function?useState<S>(initialState:?S?|?(()?=>?S)):?[S,?Dispatch<SetStateAction<S>>];
          //?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>():?[S?|?undefined,?Dispatch<SetStateAction<S?|?undefined>>];
          ??/**
          ???*?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
          ???*/

          可以看到,這里定義兩種形式,分別是有初始值和沒(méi)有初始值的形式。

          2. useEffect

          useEffect的主要作用就是處理副作用,它的第一個(gè)參數(shù)是一個(gè)函數(shù),表示要清除副作用的操作,第二個(gè)參數(shù)是一組值,當(dāng)這組值改變時(shí),第一個(gè)參數(shù)的函數(shù)才會(huì)執(zhí)行,這讓我們可以控制何時(shí)運(yùn)行函數(shù)來(lái)處理副作用:

          useEffect(
          ??()?=>?{
          ????const?subscription?=?props.source.subscribe();
          ????return?()?=>?{
          ??????subscription.unsubscribe();
          ????};
          ??},
          ??[props.source]
          );

          當(dāng)函數(shù)的返回值不是函數(shù)或者effect函數(shù)中未定義的內(nèi)容時(shí),如下:

          useEffect(
          ????()?=>?{
          ??????subscribe();
          ??????return?null;?
          ????}
          );

          TypeScript就會(huì)報(bào)錯(cuò):

          來(lái)看看useEffect在類型聲明文件中的定義:

          //?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<any>;

          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的第一個(gè)參數(shù)只允許返回一個(gè)函數(shù)。

          3. useRef

          當(dāng)使用 useRef 時(shí),我們可以訪問(wèn)一個(gè)可變的引用對(duì)象??梢詫⒊跏贾祩鬟f給 useRef,它用于初始化可變 ref 對(duì)象公開(kāi)的當(dāng)前屬性。當(dāng)我們使用useRef時(shí),需要給其指定類型:

          const?nameInput?=?React.useRef(null)

          這里給實(shí)例的類型指定為了input輸入框類型。

          當(dāng)useRef的初始值為null時(shí),有兩種創(chuàng)建的形式,第一種:

          const?nameInput?=?React.useRef(null)
          nameInput.current.innerText?=?"hello?world";

          這種形式下,ref1.current是只讀的(read-only),所以當(dāng)我們將它的innerText屬性重新賦值時(shí)會(huì)報(bào)以下錯(cuò)誤:

          Cannot?assign?to?'current'?because?it?is?a?read-only?property.

          那該怎么將current屬性變?yōu)閯?dòng)態(tài)可變的,先來(lái)看看類型聲明文件中 useRef 是如何定義的:

          ?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的直接可變,就需要在泛型參數(shù)中包含'| null',所以這就是當(dāng)初始值為null的第二種定義形式:

          const?nameInput?=?React.useRefnull>(null);

          這種形式下,nameInput.current就是可寫(xiě)的。不過(guò)兩種類型在使用時(shí)都需要做類型檢查:

          nameInput.current?.innerText?=?"hello?world";

          那么問(wèn)題來(lái)了,為什么第一種寫(xiě)法在沒(méi)有操作current時(shí)沒(méi)有報(bào)錯(cuò)呢?因?yàn)閡seRef在類型定義時(shí)具有多個(gè)重載聲明,第一種方式就是執(zhí)行的以下函數(shù)重載:

          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就是參數(shù)的類型T,所以最終nameInput 的類型就是React.MutableRefObject。

          注意,上面用到了HTMLInputElement類型,這是一個(gè)標(biāo)簽類型,這個(gè)操作就是用來(lái)訪問(wèn)DOM元素的。

          4. useCallback

          先來(lái)看看類型聲明文件中對(duì)useCallback的定義:

          ?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接收一個(gè)回調(diào)函數(shù)和一個(gè)依賴數(shù)組,只有當(dāng)依賴數(shù)組中的值發(fā)生變化時(shí)才會(huì)重新執(zhí)行回調(diào)函數(shù)。來(lái)看一個(gè)例子:

          const?add?=?(a:?number,?b:?number)?=>?a?+?b;

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

          這里我們沒(méi)有給回調(diào)函數(shù)中的參數(shù)a定義類型,所以下面的調(diào)用方式都不會(huì)報(bào)錯(cuò):

          memoizedCallback("hello");
          memoizedCallback(5)

          盡管add方法的兩個(gè)參數(shù)都是number類型,但是上述調(diào)用都能夠用執(zhí)行。所以為了更加嚴(yán)謹(jǐn),我們需要給回調(diào)函數(shù)定義具體的類型:

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

          這時(shí)候如果再給回調(diào)函數(shù)傳入字符串就會(huì)報(bào)錯(cuò)了:

          所有,需要注意,在使用useCallback時(shí)需要給回調(diào)函數(shù)的參數(shù)指定類型。

          5. useMemo

          先來(lái)看看類型聲明文件中對(duì)useMemo的定義:

          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是非常類似的,但是它返回的是一個(gè)值,而不是函數(shù)。所以在定義useMemo時(shí)需要定義返回值的類型:

          let?a?=?1;
          setTimeout(()?=>?{
          ??a?+=?1;
          },?1000);

          const?calculatedValue?=?useMemo<number>(()?=>?a?**?2,?[a]);

          如果返回值不一致,就會(huì)報(bào)錯(cuò):

          const?calculatedValue?=?useMemo<number>(()?=>?a?+?"hello",?[a]);
          //?類型“()?=>
          ?string”的參數(shù)不能賦給類型“()?=>?number”的參數(shù)

          6. useContext

          useContext需要提供一個(gè)上下文對(duì)象,并返回所提供的上下文的值,當(dāng)提供者更新上下文對(duì)象時(shí),引用這些上下文對(duì)象的組件就會(huì)重新渲染:

          const?ColorContext?=?React.createContext({?color:?"green"?});

          const?Welcome?=?()?=>?{
          ??const?{?color?}?=?useContext(ColorContext);
          ??return?hello?world</div>;
          };

          在使用useContext時(shí),會(huì)自動(dòng)推斷出提供的上下文對(duì)象的類型,所以并不需要我們手動(dòng)設(shè)置context的類型。當(dāng)前,我們也可以使用泛型來(lái)設(shè)置context的類型:

          interface?IColor?{
          ?color:?string;
          }

          const?ColorContext?=?React.createContext({?color:?"green"?});

          下面是useContext在類型聲明文件中的定義:

          function?useContext<T>(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

          有時(shí)我們需要處理一些復(fù)雜的狀態(tài),并且可能取決于之前的狀態(tài)。這時(shí)候就可以使用useReducer,它接收一個(gè)函數(shù),這個(gè)函數(shù)會(huì)根據(jù)之前的狀態(tài)來(lái)計(jì)算一個(gè)新的state。其語(yǔ)法如下:

          const?[state,?dispatch]?=?useReducer(reducer,?initialArg,?init);

          來(lái)看下面的例子:

          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}
          ??????()?=>?dispatch({type:?'increment'})}>+</button>
          ???????dispatch({type:?'decrement'})}>-button>
          ????</>
          ??);
          }

          當(dāng)前的狀態(tài)是無(wú)法推斷出來(lái)的,可以給reducer函數(shù)添加類型,通過(guò)給reducer函數(shù)定義state和action來(lái)推斷 useReducer 的類型,下面來(lái)修改上面的例子:

          type?ActionType?=?{
          ??type:?'increment'?|?'decrement';
          };

          type?State?=?{?count:?number?};

          const?initialState:?State?=?{count:?0}
          const?reducer?=?(state:?State,?action:?ActionType)?=>?{
          ??//?...
          }

          這樣,在Counter函數(shù)中就可以推斷出類型。當(dāng)我們?cè)噲D使用一個(gè)不存在的類型時(shí),就會(huì)報(bào)錯(cuò):

          dispatch({type:?'reset'});
          //?Error!?type?'"reset"'?is?not?assignable?to?type?'"increment"?|?"decrement"'

          除此之外,還可以使用泛型的形式來(lái)實(shí)現(xiàn)reducer函數(shù)的類型定義:

          type?ActionType?=?{
          ??type:?'increment'?|?'decrement';
          };

          type?State?=?{?count:?number?};

          const?reducer:?React.Reducer?=?(state,?action)?=>?{
          ??//?...
          }

          其實(shí)dispatch方法也是有類型的:

          可以看到,dispatch的類型是:React.Dispatch,上面示例的完整代碼如下:

          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}
          ??????()?=>?dispatch({?type:?"increment"?})}>+</button>
          ???????dispatch({?type:?"decrement"?})}>-button>
          ????</>
          ??);
          };

          export?default?Counter;

          四、事件處理

          1. Event 事件類型

          在開(kāi)發(fā)中我們會(huì)經(jīng)常在事件處理函數(shù)中使用event事件對(duì)象,比如在input框輸入時(shí)實(shí)時(shí)獲取輸入的值;使用鼠標(biāo)事件時(shí),通過(guò) clientX、clientY 獲取當(dāng)前指針的坐標(biāo)等等。

          我們知道,Event是一個(gè)對(duì)象,并且有很多屬性,這時(shí)很多人就會(huì)把 event 類型定義為any,這樣的話TypeScript就失去了它的意義,并不會(huì)對(duì)event事件進(jìn)行靜態(tài)檢查,如果一個(gè)鍵盤事件觸發(fā)了下面的方法,也不會(huì)報(bào)錯(cuò):

          const?handleEvent?=?(e:?any)?=>?{
          ????console.log(e.clientX,?e.clientY)
          }

          由于Event事件對(duì)象中有很多的屬性,所以我們也不方便把所有屬性及其類型定義在一個(gè)interface中,所以React在聲明文件中給我們提供了Event事件對(duì)象的類型聲明。

          常見(jiàn)的Event 事件對(duì)象如下:

          • 剪切板事件對(duì)象:ClipboardEvent
          • 拖拽事件對(duì)象:DragEvent
          • 焦點(diǎn)事件對(duì)象:FocusEvent
          • 表單事件對(duì)象:FormEvent
          • Change事件對(duì)象:ChangeEvent
          • 鍵盤事件對(duì)象:KeyboardEvent
          • 鼠標(biāo)事件對(duì)象:MouseEvent
          • 觸摸事件對(duì)象:TouchEvent
          • 滾輪事件對(duì)象:WheelEvent
          • 動(dòng)畫(huà)事件對(duì)象:AnimationEvent
          • 過(guò)渡事件對(duì)象:TransitionEvent

          可以看到,這些Event事件對(duì)象的泛型中都會(huì)接收一個(gè)Element元素的類型,這個(gè)類型就是我們綁定這個(gè)事件的標(biāo)簽元素的類型,標(biāo)簽元素類型將在下面的第五部分介紹。

          來(lái)看一個(gè)簡(jiǎn)單的例子:

          type?State?=?{
          ??text:?string;
          };

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

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

          ??????type="text"?value={text}?onChange={onChange}?/>
          ????</div>
          ??);
          }

          這里就給onChange方法的事件對(duì)象定義為了FormEvent類型,并且作用的對(duì)象是一個(gè)HTMLInputElement類型的標(biāo)簽(input標(biāo)簽)

          可以來(lái)看下MouseEvent事件對(duì)象和ChangeEvent事件對(duì)象的類型聲明,其他事件對(duì)象的聲明形似也類似:

          interface?MouseEvent?extends?UIEvent?{
          ??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;
          ??movementX:?number;
          ??movementY:?number;
          ??pageX:?number;
          ??pageY:?number;
          ??relatedTarget:?EventTarget?|?null;
          ??screenX:?number;
          ??screenY:?number;
          ??shiftKey:?boolean;
          }

          interface?ChangeEvent?extends?SyntheticEvent?{
          ??target:?EventTarget?&?T;
          }

          在很多事件對(duì)象的聲明文件中都可以看到 EventTarget 的身影。這是因?yàn)?,DOM的事件操作(監(jiān)聽(tīng)和觸發(fā)),都定義在EventTarget接口上。EventTarget 的類型聲明如下:


          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事件中,會(huì)使用的e.target來(lái)獲取當(dāng)前的值,它的的類型就是EventTarget。來(lái)看下面的例子:

          ?onChange={e?=>?onSourceChange(e)}
          ?placeholder="最多30個(gè)字"
          />

          const?onSourceChange?=?(e:?React.ChangeEvent)?=>?{
          ????if?(e.target.value.length?>?30)?{
          ??????message.error('請(qǐng)長(zhǎng)度不能超過(guò)30個(gè)字,請(qǐng)重新輸入');
          ??????return;
          ????}
          ????setSourceInput(e.target.value);
          };

          這里定義了一個(gè)input輸入框,當(dāng)觸發(fā)onChange事件時(shí),會(huì)調(diào)用onSourceChange方法,該方法的參數(shù)e的類型就是:React.ChangeEvent,而e.target的類型就是EventTarget:

          再來(lái)看一個(gè)例子:

          questionList.map(item?=>?(
          ?????????key={item.id}
          ???role="button"
          ???onClick={e?=>?handleChangeCurrent(item,?e)}
          ????>
          ????//?組件內(nèi)容...
          ????</div>
          )

          const?handleChangeCurrent?=?(item:?IData,?e:?React.MouseEvent)?=>?{
          ????e.stopPropagation();
          ????setCurrent(item);
          };

          這點(diǎn)代碼中,點(diǎn)擊某個(gè)盒子,就將它設(shè)置為當(dāng)前的盒子,方便執(zhí)行其他操作。當(dāng)鼠標(biāo)點(diǎn)擊盒子時(shí),會(huì)觸發(fā)handleChangeCurren方法,該方法有兩個(gè)參數(shù),第二個(gè)參數(shù)是event對(duì)象,在方法中執(zhí)行了e.stopPropagation();是為了阻止冒泡事件,這里的stopPropagation()實(shí)際上并不是鼠標(biāo)事件MouseEvent的屬性,它是合成事件上的屬性,來(lái)看看聲明文件中的定義:

          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()是一層層的繼承來(lái)的,最終來(lái)自于BaseSyntheticEvent合成事件類型。原生的事件集合SyntheticEvent就是繼承自合成時(shí)間類型。SyntheticEvent泛型接口接收當(dāng)前的元素類型和事件類型,如果不介意這兩個(gè)參數(shù)的類型,完全可以這樣寫(xiě):

          ??onChange={(e:?SyntheticEvent)=>{
          ????//...?
          ??}}
          />

          2. 事件處理函數(shù)類型

          說(shuō)完事件對(duì)象類型,再來(lái)看看事件處理函數(shù)的類型。React也為我們提供了貼心的提供了事件處理函數(shù)的類型聲明,來(lái)看看所有的事件處理函數(shù)的類型聲明:

          type?EventHandlerextends?SyntheticEvent<any>>?=?{?bivarianceHack(event:?E):?void?}["bivarianceHack"];

          type?ReactEventHandler?=?EventHandler>;
          //?剪切板事件處理函數(shù)
          type?ClipboardEventHandler?=?EventHandler>;
          //?復(fù)合事件處理函數(shù)
          type?CompositionEventHandler?=?EventHandler>;
          //?拖拽事件處理函數(shù)
          type?DragEventHandler?=?EventHandler>;
          //?焦點(diǎn)事件處理函數(shù)
          type?FocusEventHandler?=?EventHandler>;
          //?表單事件處理函數(shù)
          type?FormEventHandler?=?EventHandler>;
          //?Change事件處理函數(shù)
          type?ChangeEventHandler?=?EventHandler>;
          //?鍵盤事件處理函數(shù)
          type?KeyboardEventHandler?=?EventHandler>;
          //?鼠標(biāo)事件處理函數(shù)
          type?MouseEventHandler?=?EventHandler>;
          //?觸屏事件處理函數(shù)
          type?TouchEventHandler?=?EventHandler>;
          //?指針事件處理函數(shù)
          type?PointerEventHandler?=?EventHandler>;
          //?界面事件處理函數(shù)
          type?UIEventHandler?=?EventHandler>;
          //?滾輪事件處理函數(shù)
          type?WheelEventHandler?=?EventHandler>;
          //?動(dòng)畫(huà)事件處理函數(shù)
          type?AnimationEventHandler?=?EventHandler>;
          //?過(guò)渡事件處理函數(shù)
          type?TransitionEventHandler?=?EventHandler>;

          這里面的T的類型也都是Element,指的是觸發(fā)該事件的HTML標(biāo)簽元素的類型,下面第五部分會(huì)介紹。

          EventHandler會(huì)接收一個(gè)E,它表示事件處理函數(shù)中 Event 對(duì)象的類型。bivarianceHack 是事件處理函數(shù)的類型定義,函數(shù)接收一個(gè) Event 對(duì)象,并且其類型為接收到的泛型變量 E 的類型, 返回值為 void。

          還看上面的那個(gè)例子:

          type?State?=?{
          ??text:?string;
          };

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

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

          ??????type="text"?value={text}?onChange={onChange}?/>
          ????</div>
          ??);
          }

          這里給onChange方法定義了方法的類型,它是一個(gè)ChangeEventHandler的類型,并且作用的對(duì)象是一個(gè)HTMLImnputElement類型的標(biāo)簽(input標(biāo)簽)。

          五、HTML標(biāo)簽類型

          1. 常見(jiàn)標(biāo)簽類型

          在項(xiàng)目的依賴文件中可以找到HTML標(biāo)簽相關(guān)的類型聲明文件:

          所有的HTML標(biāo)簽的類型都被定義在 intrinsicElements 接口中,常見(jiàn)的標(biāo)簽及其類型如下:

          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;?

          那什么時(shí)候會(huì)使用到標(biāo)簽類型呢,上面第四部分的Event事件類型和事件處理函數(shù)類型中都使用到了標(biāo)簽的類型。上面的很多的類型都需要傳入一個(gè)ELement類型的泛型參數(shù),這個(gè)泛型參數(shù)就是對(duì)應(yīng)的標(biāo)簽類型值,可以根據(jù)標(biāo)簽來(lái)選擇對(duì)應(yīng)的標(biāo)簽類型。這些類型都繼承自HTMLElement類型,如果使用時(shí)對(duì)類型類型要求不高,可以直接寫(xiě)HTMLELement。比如下面的例子:

          ?type="text"
          ?onClick={(e:?React.MouseEvent)?=>?{
          ??handleOperate();
          ??e.stopPropagation();
          }}
          ??>
          ?????src={cancelChangeIcon}
          ?alt=""
          ????/>
          ????取消修改
          </Button>

          其實(shí),在直接操作DOM時(shí)也會(huì)用到標(biāo)簽類型,雖然我們現(xiàn)在通常會(huì)使用框架來(lái)開(kāi)發(fā),但是有時(shí)候也避免不了直接操作DOM。比如我在工作中,項(xiàng)目中的某一部分組件是通過(guò)npm來(lái)引入的其他組的組件,而在很多時(shí)候,我有需要?jiǎng)討B(tài)的去個(gè)性化這個(gè)組件的樣式,最直接的辦法就是通過(guò)原生JavaScript獲取到DOM元素,來(lái)進(jìn)行樣式的修改,這時(shí)候就會(huì)用到標(biāo)簽類型。

          來(lái)看下面的例子:

          document.querySelectorAll('.paper').forEach(item?=>?{
          ??const?firstPageHasAddEle?=?(item.firstChild?as?HTMLDivElement).classList.contains('add-ele');
          ??
          ??if?(firstPageHasAddEle)?{
          ????item.removeChild(item.firstChild?as?ChildNode);
          ??}
          })

          這是我最近寫(xiě)的一段代碼(略微刪改),在第一頁(yè)有個(gè)add-ele元素的時(shí)候就刪除它。這里我們將item.firstChild斷言成了HTMLDivElement類型,如果不斷言,item.firstChild的類型就是ChildNode,而ChildNode類型中是不存在classList屬性的,所以就就會(huì)報(bào)錯(cuò),當(dāng)我們把他斷言成HTMLDivElement類型時(shí),就不會(huì)報(bào)錯(cuò)了。很多時(shí)候,標(biāo)簽類型可以和斷言(as)一起使用。

          后面在removeChild時(shí)又使用了as斷言,為什么呢?item.firstChild不是已經(jīng)自動(dòng)識(shí)別為ChildNode類型了嗎?因?yàn)門S會(huì)認(rèn)為,我們可能不能獲取到類名為paper的元素,所以item.firstChild的類型就被推斷為ChildNode | null,我們有時(shí)候比TS更懂我們定義的元素,知道頁(yè)面一定存在paper 元素,所以可以直接將item.firstChild斷言成ChildNode類型。

          2. 標(biāo)簽屬性類型

          眾所周知,每個(gè)HTML標(biāo)簽都有自己的屬性,比如Input框就有value、width、placeholder、max-length等屬性,下面是Input框的屬性類型定義:

          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<string>?|?number?|?undefined;
          ??width?:?number?|?string?|?undefined;

          ??onChange?:?ChangeEventHandler?|?undefined;
          }

          如果我們需要直接操作DOM,就可能會(huì)用到元素屬性類型,常見(jiàn)的元素屬性類型如下:

          • HTML屬性類型:HTMLAttributes
          • 按鈕屬性類型:ButtonHTMLAttributes
          • 表單屬性類型:FormHTMLAttributes
          • 圖片屬性類型:ImgHTMLAttributes
          • 輸入框?qū)傩灶愋停篒nputHTMLAttributes
          • 鏈接屬性類型:LinkHTMLAttributes
          • meta屬性類型:MetaHTMLAttributes
          • 選擇框?qū)傩灶愋停篠electHTMLAttributes
          • 表格屬性類型:TableHTMLAttributes
          • 輸入?yún)^(qū)屬性類型:TextareaHTMLAttributes
          • 視頻屬性類型:VideoHTMLAttributes
          • SVG屬性類型:SVGAttributes
          • WebView屬性類型:WebViewHTMLAttributes

          一般情況下,我們是很少需要在項(xiàng)目中顯式的去定義標(biāo)簽屬性的類型。如果子級(jí)去封裝組件庫(kù)的話,這些屬性就能發(fā)揮它們的作用了。來(lái)看例子(來(lái)源于網(wǎng)絡(luò),僅供學(xué)習(xí)):

          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標(biāo)簽?的屬性

          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?標(biāo)簽才有?disabled?類名,button沒(méi)有
          ????})

          ????if(btnType?===?ButtonType.Link?&&?href)?{
          ????????return?(
          ?????????????????????????className={classes}
          ?????????????href={href}
          ?????????????{...restProps}
          ????????????>
          ????????????????{children}
          ????????????</a>
          ????????)

          ????}?else?{
          ????????return?(
          ?????????????????????????className={classes}
          ?????????????disabled={disabled}?/
          /?button元素默認(rèn)有disabled屬性,所以即便沒(méi)給他設(shè)置樣式也會(huì)和普通button有一定區(qū)別

          ?????????????{...restProps}
          ????????????>
          ????????????????{children}
          ????????????button>
          ????????)
          ????}
          }

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

          export?default?Button;

          這段代碼就是用來(lái)封裝一個(gè)buttom按鈕,在button的基礎(chǔ)上添加了一些自定義屬性,比如上面將button的類型使用交叉類型(&)獲得自定義屬性和原生 button 屬性 :

          type?NativeButtonProps?=?BaseButtonProps?&?React.ButtonHTMLAttributes?

          可以看到,標(biāo)簽屬性類型在封裝組件庫(kù)時(shí)還是很有用的,更多用途可以自己探索~

          六、工具泛型

          在項(xiàng)目中使用一些工具泛型可以提高我們的開(kāi)發(fā)效率,少寫(xiě)很多類型定義。下面來(lái)看看有哪些常見(jiàn)的工具泛型,以及其使用方式。

          1. Partial

          Partial 作用是將傳入的屬性變?yōu)榭蛇x項(xiàng)。適用于對(duì)類型結(jié)構(gòu)不明確的情況。它使用了兩個(gè)關(guān)鍵字:keyof和in,先來(lái)看看他們都是什么含義。keyof 可以用來(lái)取得接口的所有 key 值:

          interface?IPerson?{
          ??name:?string;
          ??age:?number;
          ??height:?number;
          }
          type?T?=?keyof?IPerson?
          // T 類型為:?"name"?|?"age"?|?"number"

          in關(guān)鍵字可以遍歷枚舉類型,:

          type?Person?=?"name"?|?"age"?|?"number"
          type?Obj?=??{
          ??[p?in?Keys]:?any
          }?
          // Obj類型為:?{ name: any, age: any, number: any }

          keyof 可以產(chǎn)生聯(lián)合類型, in 可以遍歷枚舉類型, 所以經(jīng)常一起使用, 下面是Partial工具泛型的定義:

          /**
          ?*?Make?all?properties?in?T?optional
          ?*?將T中的所有屬性設(shè)置為可選
          ?*/

          type?Partial?=?{
          ????[P?in?keyof?T]?:?T[P];
          };

          這里,keyof T 獲取 T 所有屬性名, 然后使用 in 進(jìn)行遍歷, 將值賦給 P, 最后 T[P] 取得相應(yīng)屬性的值。中間的?就用來(lái)將屬性設(shè)置為可選。

          使用示例如下:

          interface?IPerson?{
          ??name:?string;
          ??age:?number;
          ??height:?number;
          }

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

          2. Required

          Required 的作用是將傳入的屬性變?yōu)楸剡x項(xiàng),和上面的工具泛型恰好相反,其聲明如下:

          /**
          ?*?Make?all?properties?in?T?required
          ?*?將T中的所有屬性設(shè)置為必選
          ?*/

          type?Required?=?{
          ????[P?in?keyof?T]-?:?T[P];
          };

          可以看到,這里使用-?將屬性設(shè)置為必選,可以理解為減去問(wèn)號(hào)。使用形式和上面的Partial差不多:

          interface?IPerson?{
          ??name?:?string;
          ??age?:?number;
          ??height?:?number;
          }

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

          3. Readonly

          將T類型的所有屬性設(shè)置為只讀(readonly),構(gòu)造出來(lái)類型的屬性不能被再次賦值。Readonly的聲明形式如下:

          /**
          ?*?Make?all?properties?in?T?readonly
          ?*/

          type?Readonly?=?{
          ????readonly?[P?in?keyof?T]:?T[P];
          };

          使用示例如下:

          interface?IPerson?{
          ??name:?string;
          ??age:?number;
          }

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

          person.age?=?20;??//??Error:?cannot?reassign?a?readonly?property

          可以看到,通過(guò) Readonly將IPerson的屬性轉(zhuǎn)化成了只讀,不能再進(jìn)行賦值操作。

          4. Pick

          從T類型中挑選部分屬性K來(lái)構(gòu)造新的類型。它的聲明形式如下:

          /**
          ?*?From?T,?pick?a?set?of?properties?whose?keys?are?in?the?union?K
          ?*/

          type?Pickextends?keyof?T>?=?{
          ????[P?in?K]:?T[P];
          };

          使用示例如下:

          interface?IPerson?{
          ??name:?string;
          ??age:?number;
          ??height:?number;
          }

          const?person:?Pick"name"?|?"age">?=?{
          ??name:?"zhangsan",
          ??age:?18
          }

          5. Record

          Record 用來(lái)構(gòu)造一個(gè)類型,其屬性名的類型為K,屬性值的類型為T。這個(gè)工具泛型可用來(lái)將某個(gè)類型的屬性映射到另一個(gè)類型上,下面是其聲明形式:

          /**
          ?*?Construct?a?type?with?a?set?of?properties?K?of?type?T
          ?*/

          type?Recordextends?keyof?any,?T>?=?{
          ????[P?in?K]:?T;
          };

          使用示例如下:

          interface?IPageinfo?{
          ????title:?string;
          }

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

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

          6. Exclude

          Exclude 就是從一個(gè)聯(lián)合類型中排除掉屬于另一個(gè)聯(lián)合類型的子集,下面是其聲明的形式:

          /**
          ?*?Exclude?from?T?those?types?that?are?assignable?to?U
          ?*/

          type?Exclude?=?T?extends?U???never?:?T;

          使用示例如下:

          interface?IPerson?{
          ??name:?string;
          ??age:?number;
          ??height:?number;
          }

          const?person:?Exclude"age"?|?"sex">?=?{
          ??name:?"zhangsan";
          ??height:?180;
          }

          7. Omit

          上面的Pick 和 Exclude 都是最基礎(chǔ)基礎(chǔ)的工具泛型,很多時(shí)候用 Pick 或者 Exclude 還不如直接寫(xiě)類型更直接。而 Omit 就基于這兩個(gè)來(lái)做的一個(gè)更抽象的封裝,它允許從一個(gè)對(duì)象中剔除若干個(gè)屬性,剩下的就是需要的新類型。下面是它的聲明形式:

          /**
          ?*?Construct?a?type?with?the?properties?of?T?except?for?those?in?type?K.
          ?*/

          type?Omitextends?keyof?any>?=?Pick>;

          使用示例如下:

          interface?IPerson?{
          ??name:?string;
          ??age:?number;
          ??height:?number;
          }

          const?person:?Omit"age"?|?"height">?=?{
          ??name:?"zhangsan";
          }

          8. ReturnType

          ReturnType會(huì)返回函數(shù)返回值的類型,其聲明形式如下:

          /**
          ?*?Obtain?the?return?type?of?a?function?type
          ?*/

          type?ReturnTypeextends?(...args:?any)?=>?any>?=?T?extends?(...args:?any)?=>?infer?R???R?:?any;

          使用示例如下:

          function?foo(type):?boolean?{
          ??return?type?===?0
          }

          type?FooType?=?ReturnType<typeof?foo>

          這里使用 typeof 是為了獲取 foo 的函數(shù)簽名,等價(jià)于 (type: any) => boolean。

          七、Axios 封裝

          在React項(xiàng)目中,我們經(jīng)常使用Axios庫(kù)進(jìn)行數(shù)據(jù)請(qǐng)求,Axios 是基于 Promise 的 HTTP 庫(kù),可以在瀏覽器和 node.js 中使用。Axios 具備以下特性:

          • 從瀏覽器中創(chuàng)建 XMLHttpRequests;
          • 從 node.js 創(chuàng)建 HTTP 請(qǐng)求;
          • 支持 Promise API;
          • 攔截請(qǐng)求和響應(yīng);
          • 轉(zhuǎn)換請(qǐng)求數(shù)據(jù)和響應(yīng)數(shù)據(jù);
          • 取消請(qǐng)求;
          • 自動(dòng)轉(zhuǎn)換 JSON 數(shù)據(jù);
          • 客戶端支持防御 XSRF。

          Axios的基本使用就不再多介紹了。為了更好地調(diào)用,做一些全局的攔截,通常會(huì)對(duì)Axios進(jìn)行封裝,下面就使用TypeScript對(duì)Axios進(jìn)行簡(jiǎn)單封裝,使其同時(shí)能夠有很好的類型支持。Axios是自帶聲明文件的,所以我們無(wú)需額外的操作。

          下面來(lái)看基本的封裝:

          import?axios,?{?AxiosInstance,?AxiosRequestConfig,?AxiosPromise,AxiosResponse?}?from?'axios';?//?引入axios和定義在node_modules/axios/index.ts文件里的類型聲明

          ?//?定義接口請(qǐng)求類,用于創(chuàng)建axios請(qǐng)求實(shí)例
          class?HttpRequest?{
          ??//?接收接口請(qǐng)求的基本路徑
          ??constructor(public?baseUrl:?string)?{?
          ????this.baseUrl?=?baseUrl;
          ??}
          ??
          ??//?調(diào)用接口時(shí)調(diào)用實(shí)例的這個(gè)方法,返回AxiosPromise
          ??public?request(options:?AxiosRequestConfig):?AxiosPromise?{?
          ????//?創(chuàng)建axios實(shí)例,它是函數(shù),同時(shí)這個(gè)函數(shù)包含多個(gè)屬性
          ????const?instance:?AxiosInstance?=?axios.create()?
          ????//?合并基礎(chǔ)路徑和每個(gè)接口單獨(dú)傳入的配置,比如url、參數(shù)等
          ????options?=?this.mergeConfig(options)?
          ????//?調(diào)用interceptors方法使攔截器生效
          ????this.interceptors(instance,?options.url)?
          ????//?返回AxiosPromise
          ????return?instance(options)?
          ??}
          ??
          ??//?用于添加全局請(qǐng)求和響應(yīng)攔截
          ??private?interceptors(instance:?AxiosInstance,?url?:?string)?{?
          ????//?請(qǐng)求和響應(yīng)攔截
          ??}
          ??
          ??//?用于合并基礎(chǔ)路徑配置和接口單獨(dú)配置
          ??private?mergeConfig(options:?AxiosRequestConfig):?AxiosRequestConfig?{?
          ????return?Object.assign({?baseURL:?this.baseUrl?},?options);
          ??}
          }
          export?default?HttpRequest;

          通常baseUrl在開(kāi)發(fā)環(huán)境的和生產(chǎn)環(huán)境的路徑是不一樣的,所以可以根據(jù)當(dāng)前是開(kāi)發(fā)環(huán)境還是生產(chǎn)環(huán)境做判斷,應(yīng)用不同的基礎(chǔ)路徑。這里要寫(xiě)在一個(gè)配置文件里:

          export?default?{
          ????api:?{
          ????????devApiBaseUrl:?'/test/api/xxx',
          ????????proApiBaseUrl:?'/api/xxx',
          ????},
          };

          在上面的文件中引入這個(gè)配置:

          import?{?api:?{?devApiBaseUrl,?proApiBaseUrl?}?}?from?'@/config';
          const?apiBaseUrl?=?env.NODE_ENV?===?'production'???proApiBaseUrl?:?devApiBaseUrl;

          之后就可以將apiBaseUrl作為默認(rèn)值傳入HttpRequest的參數(shù):

          class?HttpRequest?{?
          ??constructor(public?baseUrl:?string?=?apiBaseUrl)?{?
          ????this.baseUrl?=?baseUrl;
          ??}

          接下來(lái)可以完善一下攔截器類,在類中interceptors方法內(nèi)添加請(qǐng)求攔截器和響應(yīng)攔截器,實(shí)現(xiàn)對(duì)所有接口請(qǐng)求的統(tǒng)一處理:

          private?interceptors(instance:?AxiosInstance,?url?:?string)?{
          ???//?請(qǐng)求攔截
          ????instance.interceptors.request.use((config:?AxiosRequestConfig)?=>?{
          ??????//?接口請(qǐng)求的所有配置,可以在axios.defaults修改配置
          ??????return?config
          ????},
          ????(error)?=>?{
          ??????return?Promise.reject(error)
          ????})
          ??
          ???//?響應(yīng)攔截
          ????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)
          ????})
          ??}

          到這里封裝的就差不多了,一般服務(wù)端會(huì)將狀態(tài)碼、提示信息和數(shù)據(jù)封裝在一起,然后作為數(shù)據(jù)返回,所以所有請(qǐng)求返回的數(shù)據(jù)格式都是一樣的,所以就可以定義一個(gè)接口來(lái)指定返回的數(shù)據(jù)結(jié)構(gòu),可以定義一個(gè)接口:

          export?interface?ResponseData?{
          ??code:?number
          ??data?:?any
          ??msg:?string
          }

          接下來(lái)看看使用TypeScript封裝的Axios該如何使用??梢韵榷x一個(gè)請(qǐng)求實(shí)例:

          import?HttpRequest?from?'@/utils/axios'
          export?*?from?'@/utils/axios'
          export?default?new?HttpRequest()

          這里把請(qǐng)求類導(dǎo)入進(jìn)來(lái),默認(rèn)導(dǎo)出這個(gè)類的實(shí)例。之后創(chuàng)建一個(gè)登陸接口請(qǐng)求方法:

          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'
          ??})
          }

          這里封裝登錄請(qǐng)求方法loginReq,他的參數(shù)必須是我們定義的ILogin接口的類型。這個(gè)方法返回一個(gè)類型為AxiosPromise的Promise,AxiosPromise是axios聲明文件內(nèi)置的類型,可以傳入一個(gè)泛型變量參數(shù),用于指定返回的結(jié)果中data字段的類型。

          接下來(lái)可以調(diào)用一下這個(gè)登錄的接口:

          import?{?loginReq?}?from?'@/api/user'

          const?Home:?FC?=?()?=>?{
          ??const?login?=?(params)?=>?{
          ???loginReq(params).then((res)?=>?{
          ?????console.log(res.data.code)
          ???})?
          ??}??
          }

          通過(guò)這種方式,當(dāng)我們調(diào)用loginReq接口時(shí),就會(huì)提示我們,參數(shù)的類型是ILogin,需要傳入幾個(gè)參數(shù)。這樣編寫(xiě)代碼的體驗(yàn)就會(huì)好很多。

          八. 其他

          1. import React

          在React項(xiàng)目中使用TypeScript時(shí),普通組件文件后綴為.tsx,公共方法文件后綴為.ts。在. tsx 文件中導(dǎo)入 React 的方式如下:

          import?*?as?React?from?'react'
          import?*?as?ReactDOM?from?'react-dom'

          這是一種面向未來(lái)的導(dǎo)入方式,如果想在項(xiàng)目中使用以下導(dǎo)入方式:

          import?React?from?"react";
          import?ReactDOM?from?"react-dom";

          就需要在tsconfig.json配置文件中進(jìn)行如下配置:

          "compilerOptions":?{
          ????//?允許默認(rèn)從沒(méi)有默認(rèn)導(dǎo)出的模塊導(dǎo)入。
          ????"allowSyntheticDefaultImports":?true,
          }

          2. Types or Interfaces?

          我們可以使用types或者Interfaces來(lái)定義類型嗎,那么該如何選擇他倆呢?建議如下:

          • 在定義公共 API 時(shí)(比如編輯一個(gè)庫(kù))使用 interface,這樣可以方便使用者繼承接口,這樣允許使用者通過(guò)聲明合并來(lái)擴(kuò)展它們;
          • 在定義組件屬性(Props)和狀態(tài)(State)時(shí),建議使用 type,因?yàn)?type 的約束性更強(qiáng)。

          interface 和 type 在 ts 中是兩個(gè)不同的概念,但在 React 大部分使用的 case 中,interface 和 type 可以達(dá)到相同的功能效果,type 和 interface 最大的區(qū)別是:type 類型不能二次編輯,而 interface 可以隨時(shí)擴(kuò)展:

          interface?Animal?{
          ??name:?string
          }

          //?可以繼續(xù)在原屬性基礎(chǔ)上,添加新屬性:color
          interface?Animal?{
          ??color:?string
          }

          type?Animal?=?{
          ??name:?string
          }
          //?type類型不支持屬性擴(kuò)展
          //?Error:?Duplicate?identifier?'Animal'
          type?Animal?=?{
          ??color:?string
          }

          type對(duì)于聯(lián)合類型是很有用的,比如:type Type = TypeA | TypeB。而interface更適合聲明字典類行,然后定義或者擴(kuò)展它。

          3. 懶加載類型

          如果我們想在React router中使用懶加載,React也為我們提供了懶加載方法的類型,來(lái)看下面的例子:

          export?interface?RouteType?{
          ????pathname:?string;
          ????component:?LazyExoticComponent<any>;
          ????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方法在聲明文件中的定義:

          type?LazyExoticComponentextends?ComponentType<any>>?=?ExoticComponent>?&?{
          ??readonly?_result:?T;
          };

          function?lazy<T?extends?ComponentType<any>>(
          factory:?()?=>?Promise<{?default:?T?}>
          ):?LazyExoticComponent<T>
          ;

          4. 類型斷言

          類型斷言(Type Assertion)可以用來(lái)手動(dòng)指定一個(gè)值的類型。在React項(xiàng)目中,斷言還是很有用的,。有時(shí)候推斷出來(lái)的類型并不是真正的類型,很多時(shí)候我們可能會(huì)比TS更懂我們的代碼,所以可以使用斷言(使用as關(guān)鍵字)來(lái)定義一個(gè)值得類型。

          來(lái)看下面的例子:

          const?getLength?=?(target:?string?|?number):?number?=>?{
          ??if?(target.length)?{?//?error?類型"string?|?number"上不存在屬性"length"
          ????return?target.length;?//?error??類型"number"上不存在屬性"length"
          ??}?else?{
          ????return?target.toString().length;
          ??}
          };

          當(dāng)TypeScript不確定一個(gè)聯(lián)合類型的變量到底是哪個(gè)類型時(shí),就只能訪問(wèn)此聯(lián)合類型的所有類型里共有的屬性或方法,所以現(xiàn)在加了對(duì)參數(shù)target和返回值的類型定義之后就會(huì)報(bào)錯(cuò)。這時(shí)就可以使用斷言,將target的類型斷言成string類型:

          const?getStrLength?=?(target:?string?|?number):?number?=>?{
          ??if?((target?as?string).length)?{??????
          ????return?(target?as?string).length;?
          ??}?else?{
          ????return?target.toString().length;
          ??}
          };

          需要注意,類型斷言并不是類型轉(zhuǎn)換,斷言成一個(gè)聯(lián)合類型中不存在的類型是不允許的。

          再來(lái)看一個(gè)例子,在調(diào)用一個(gè)方法時(shí)傳入?yún)?shù):這里就提示我們這個(gè)參數(shù)可能是undefined,而通過(guò)業(yè)務(wù)知道這個(gè)值是一定存在的,所以就可以將它斷言成數(shù)字:data?.subjectId as number

          除此之外,上面所說(shuō)的標(biāo)簽類型、組件類型、時(shí)間類型都可以使用斷言來(lái)指定給一些數(shù)據(jù),還是要根據(jù)實(shí)際的業(yè)務(wù)場(chǎng)景來(lái)使用。

          感悟:使用類型斷言真的能解決項(xiàng)目中的很多報(bào)錯(cuò)~

          5. 枚舉類型

          枚舉類型在項(xiàng)目中的作用也是不可忽視的,使用枚舉類型可以讓代碼的擴(kuò)展性更好,當(dāng)我想更改某屬性值時(shí),無(wú)需去全局更改這個(gè)屬性,只要更改枚舉中的值即可。通常情況下,最好新建一個(gè)文件專門來(lái)定義枚舉值,便于引用。

          關(guān)于在React項(xiàng)目中如何優(yōu)雅的使用TypeScript就先介紹這么多,后面有新的內(nèi)容會(huì)再分享給大家。如果覺(jué)得不錯(cuò)就點(diǎn)個(gè)贊吧!

          Node 社群


          我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對(duì)Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。


          ???“分享、點(diǎn)贊在看” 支持一波??

          瀏覽 87
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  爱爱免费看片 | 在线免费看黄色 | 强伦人妻一区二区三区 | 精品A∨一区二区E区 | 亚洲精品中文字幕无码 |