如何優(yōu)雅地在 React 中使用TypeScript,看這一篇就夠了!
大廠技術(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.Component 和 React.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}
??????
當(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
評(píng)論圖片表情評(píng)價(jià)
爱爱免费看片
|
在线免费看黄色
|
强伦人妻一区二区三区
|
精品A∨一区二区E区
|
亚洲精品中文字幕无码
|
