金玲姐 發表於 2023-2-24 17:10:00

如何优雅地在 React 中使用TypeScript,看这一篇就够了!

<p>工作用的技术栈主要是React hooks + TypeScript。使用三月有余,其实在单独使用 TypeScript 时没有太多的坑,不过和React结合之后就会复杂很多。本文就来聊一聊TypeScript与React一起使用时经常遇到的一些类型定义的问题。阅读本文前,希望你能有一定的React和TypeScript基础。</p>
<h2>一、组件声明</h2>
<p>在React中,组件的声明方式有两种:函数组件和类组件,&nbsp;来看看这两种类型的组件声明时是如何定义TS类型的。</p>
<h3>1. 类组件</h3>
<p>类组件的定义形式有两种:<code>React.Component&lt;P, S={}&gt;</code>&nbsp;和&nbsp;<code>React.PureComponent&lt;P, S={} SS={}&gt;</code>,它们都是泛型接口,接收两个参数,第一个是props类型的定义,第二个是state类型的定义,这两个参数都不是必须的,没有时可以省略:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">interface IProps {
name: string;
}

interface IState {
count: number;
}

class App extends React.Component</span>&lt;IProps, IState&gt;<span style="color: rgba(0, 0, 0, 1)"> {
state </span>=<span style="color: rgba(0, 0, 0, 1)"> {
    count: </span>0<span style="color: rgba(0, 0, 0, 1)">
};

render() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
      </span>&lt;div&gt;<span style="color: rgba(0, 0, 0, 1)">
      {</span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.state.count}
      {</span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.props.name}
      </span>&lt;/div&gt;
<span style="color: rgba(0, 0, 0, 1)">    );
}
}

export </span><span style="color: rgba(0, 0, 255, 1)">default</span> App;</pre>
</div>
<p><code>React.PureComponent&lt;P, S={} SS={}&gt;</code>&nbsp;也是差不多的:</p>
<div class="cnblogs_code">
<pre>class App extends React.PureComponent&lt;IProps, IState&gt; {}</pre>
</div>
<p><code>React.PureComponent</code>是有第三个参数的,它表示<code>getSnapshotBeforeUpdate</code>的返回值。</p>
<p>那PureComponent和Component 的区别是什么呢?它们的主要区别是PureComponent中的shouldComponentUpdate 是由自身进行处理的,不需要我们自己处理,所以PureComponent可以在一定程度上提升性能。</p>
<p>有时候可能会见到这种写法,实际上和上面的效果是一样的:</p>
<div class="cnblogs_code">
<pre>import React, {PureComponent, Component} from "react"<span style="color: rgba(0, 0, 0, 1)">;

class App extends PureComponent</span>&lt;IProps, IState&gt;<span style="color: rgba(0, 0, 0, 1)"> {}

class App extends Component</span>&lt;IProps, IState&gt; {}</pre>
</div>
<p>那如果定义时候我们不知道组件的props的类型,只有在调用时才知道组件类型,该怎么办呢?这时泛型就发挥作用了:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 定义组件</span>
class MyComponent&lt;P&gt; extends React.Component&lt;P&gt;<span style="color: rgba(0, 0, 0, 1)"> {
internalProp: P;
constructor(props: P) {
    super(props);
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.internalProp =<span style="color: rgba(0, 0, 0, 1)"> props;
}
render() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
      </span>&lt;span&gt;hello world&lt;/span&gt;
<span style="color: rgba(0, 0, 0, 1)">    );
}
}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 使用组件</span>
type IProps =<span style="color: rgba(0, 0, 0, 1)"> { name: string; age: number; };

</span>&lt;MyComponent&lt;IProps&gt; name="React" age={18} /&gt;;          //<span style="color: rgba(0, 0, 0, 1)"> Success
</span>&lt;MyComponent&lt;IProps&gt; name="TypeScript" age="hello" /&gt;;// Error</pre>
</div>
<h3>2. 函数组件</h3>
<p>通常情况下,函数组件我是这样写的:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">interface IProps {
name: string
}

const App </span>= (props: IProps) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
const {name} </span>=<span style="color: rgba(0, 0, 0, 1)"> props;

</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
    </span>&lt;div className="App"&gt;
      &lt;h1&gt;hello world&lt;/h1&gt;
      &lt;h2&gt;{name}&lt;/h2&gt;
    &lt;/div&gt;
<span style="color: rgba(0, 0, 0, 1)">);
}

export </span><span style="color: rgba(0, 0, 255, 1)">default</span> App;</pre>
</div>
<p>除此之外,函数类型还可以使用<code>React.FunctionComponent&lt;P={}&gt;</code>来定义,也可以使用其简写<code>React.FC&lt;P={}&gt;</code>,两者效果是一样的。它是一个泛型接口,可以接收一个参数,参数表示props的类型,这个参数不是必须的。它们就相当于这样:</p>
<div class="cnblogs_code">
<pre>type React.FC&lt;P = {}&gt; = React.FunctionComponent&lt;P&gt;</pre>
</div>
<p>最终的定义形式如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">interface IProps {
name: string
}

const App: React.FC</span>&lt;IProps&gt; = (props) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
const {name} </span>=<span style="color: rgba(0, 0, 0, 1)"> props;
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
    </span>&lt;div className="App"&gt;
      &lt;h1&gt;hello world&lt;/h1&gt;
      &lt;h2&gt;{name}&lt;/h2&gt;
    &lt;/div&gt;
<span style="color: rgba(0, 0, 0, 1)">);
}

export </span><span style="color: rgba(0, 0, 255, 1)">default</span> App;</pre>
</div>
<p>当使用这种形式来定义函数组件时,props中默认会带有children属性,它表示该组件在调用时,其内部的元素,来看一个例子,首先定义一个组件,组件中引入了Child1和Child2组件:</p>
<div class="cnblogs_code">
<pre>import Child1 from "./child1"<span style="color: rgba(0, 0, 0, 1)">;
import Child2 from </span>"./child2"<span style="color: rgba(0, 0, 0, 1)">;

interface IProps {
name: string;
}
const App: React.FC</span>&lt;IProps&gt; = (props) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
const { name } </span>=<span style="color: rgba(0, 0, 0, 1)"> props;
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
    </span>&lt;Child1 name={name}&gt;
      &lt;Child2 name={name} /&gt;
<span style="color: rgba(0, 0, 0, 1)">      TypeScript
    </span>&lt;/Child1&gt;
<span style="color: rgba(0, 0, 0, 1)">);
};

export </span><span style="color: rgba(0, 0, 255, 1)">default</span> App;</pre>
</div>
<p>Child1组件结构如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">interface IProps {
name: string;
}
const Child1: React.FC</span>&lt;IProps&gt; = (props) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
const { name, children } </span>=<span style="color: rgba(0, 0, 0, 1)"> props;
console.log(children);
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
    </span>&lt;div className="App"&gt;
      &lt;h1&gt;hello child1&lt;/h1&gt;
      &lt;h2&gt;{name}&lt;/h2&gt;
    &lt;/div&gt;
<span style="color: rgba(0, 0, 0, 1)">);
};

export </span><span style="color: rgba(0, 0, 255, 1)">default</span> Child1;</pre>
</div>
<p>我们在Child1组件中打印了children属性,它的值是一个数组,包含Child2对象和后面的文本:</p>
<div><img src="https://img2023.cnblogs.com/blog/1022151/202302/1022151-20230224161437763-232885803.png">
<p>使用 React.FC 声明函数组件和普通声明的区别如下:</p>
<ul>
<li>
<p>React.FC 显式地定义了返回类型,其他方式是隐式推导的;</p>
</li>
<li>
<p>React.FC 对静态属性:displayName、propTypes、defaultProps 提供了类型检查和自动补全;</p>
</li>
<li>
<p>React.FC 为 children 提供了隐式的类型(ReactElement | null)。</p>
</li>
</ul>
<p>那如果我们在定义组件时不知道props的类型,只有调用时才知道,那就还是用泛型来定义props的类型。对于使用function定义的函数组件:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 定义组件</span>
<span style="color: rgba(0, 0, 255, 1)">function</span> MyComponent&lt;P&gt;<span style="color: rgba(0, 0, 0, 1)">(props: P) {
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
   </span>&lt;span&gt;<span style="color: rgba(0, 0, 0, 1)">
   {props}
    </span>&lt;/span&gt;
<span style="color: rgba(0, 0, 0, 1)">);
}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 使用组件</span>
type IProps =<span style="color: rgba(0, 0, 0, 1)"> { name: string; age: number; };

</span>&lt;MyComponent&lt;IProps&gt; name="React" age={18} /&gt;;          //<span style="color: rgba(0, 0, 0, 1)"> Success
</span>&lt;MyComponent&lt;IProps&gt; name="TypeScript" age="hello" /&gt;;// Error</pre>
</div>
<p>如果使用箭头函数定义的函数组件,直接这样调用是错误的:</p>
<div class="cnblogs_code">
<pre>const MyComponent = &lt;P&gt;<span style="color: rgba(0, 0, 0, 1)">(props: P) {
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
   </span>&lt;span&gt;<span style="color: rgba(0, 0, 0, 1)">
   {props}
    </span>&lt;/span&gt;
<span style="color: rgba(0, 0, 0, 1)">);
}</span></pre>
</div>
<p>必须使用extends关键字来定义泛型参数才能被成功解析:</p>
<div class="cnblogs_code">
<pre>const MyComponent = &lt;P extends any&gt;<span style="color: rgba(0, 0, 0, 1)">(props: P) {
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
   </span>&lt;span&gt;<span style="color: rgba(0, 0, 0, 1)">
   {props}
    </span>&lt;/span&gt;
<span style="color: rgba(0, 0, 0, 1)">);
}</span></pre>
</div>
<h2>二、React内置类型</h2>
<h3>1. JSX.Element</h3>
<p>先来看看JSX.Element类型的声明:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">declare global {
namespace JSX {
    interface Element extends React.ReactElement</span>&lt;any, any&gt;<span style="color: rgba(0, 0, 0, 1)"> { }
}
}</span></pre>
</div>
<p>可以看到,JSX.Element是ReactElement的子类型,它没有增加属性,两者是等价的。也就是说两种类型的变量可以相互赋值。</p>
<p>JSX.Element 可以通过执行 React.createElement 或是转译 JSX 获得:</p>
<div class="cnblogs_code">
<pre>const jsx = &lt;div&gt;hello&lt;/div&gt;
const ele = React.createElement("div", <span style="color: rgba(0, 0, 255, 1)">null</span>, "hello");</pre>
</div>
<h3>2. React.ReactElement</h3>
<p>React 的类型声明文件中提供了 React.ReactElement<T>,它可以让我们通过传入<T/>来注解类组件的实例化,它在声明文件中的定义如下:</p>
<div class="cnblogs_code">
<pre>interface ReactElement&lt;P = any, T extends string | JSXElementConstructor&lt;any&gt; = string | JSXElementConstructor&lt;any&gt;&gt;<span style="color: rgba(0, 0, 0, 1)"> {
   type: T;
   props: P;
   key: Key </span>| <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">;
}</span></pre>
</div>
<p>ReactElement是一个接口,包含type,props,key三个属性值。该类型的变量值只能是两种:null 和 ReactElement实例。</p>
<p>通常情况下,函数组件返回ReactElement(JXS.Element)的值。</p>
<h3>3. React.ReactNode</h3>
<p>ReactNode类型的声明如下:</p>
<div class="cnblogs_code">
<pre>type ReactText = string |<span style="color: rgba(0, 0, 0, 1)"> number;
type ReactChild </span>= ReactElement |<span style="color: rgba(0, 0, 0, 1)"> ReactText;

interface ReactNodeArray extends Array</span>&lt;ReactNode&gt;<span style="color: rgba(0, 0, 0, 1)"> {}
type ReactFragment </span>= {} |<span style="color: rgba(0, 0, 0, 1)"> ReactNodeArray;
type ReactNode </span>= ReactChild | ReactFragment | ReactPortal | <span style="color: rgba(0, 0, 255, 1)">boolean</span> | <span style="color: rgba(0, 0, 255, 1)">null</span> | undefined;</pre>
</div>
<p>可以看到,ReactNode是一个联合类型,它可以是string、number、ReactElement、null、boolean、ReactNodeArray。由此可知。ReactElement类型的变量可以直接赋值给ReactNode类型的变量,但反过来是不行的。</p>
<p>类组件的 render 成员函数会返回 ReactNode 类型的值:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">class MyComponent extends React.Component {
render() {
   </span><span style="color: rgba(0, 0, 255, 1)">return</span> &lt;div&gt;hello world&lt;/div&gt;
<span style="color: rgba(0, 0, 0, 1)">    }
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 正确</span>
const component: React.ReactNode&lt;MyComponent&gt; = &lt;MyComponent /&gt;;
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 错误</span>
const component: React.ReactNode&lt;MyComponent&gt; = &lt;OtherComponent /&gt;;</pre>
</div>
<p>上面的代码中,给component变量设置了类型是Mycomponent类型的react实例,这时只能给其赋值其为MyComponent的实例组件。</p>
<p>通常情况下,类组件通过 render() 返回 ReactNode的值。</p>
<h3>4. CSSProperties</h3>
<p>先来看看React的声明文件中对CSSProperties 的定义:</p>
<div class="cnblogs_code">
<pre>export interface CSSProperties extends CSS.Properties&lt;string | number&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
   * 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
   </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
}</span></pre>
</div>
<p>React.CSSProperties是React基于TypeScript定义的CSS属性类型,可以将一个方法的返回值设置为该类型:</p>
<div class="cnblogs_code">
<pre>import * as React from "react"<span style="color: rgba(0, 0, 0, 1)">;

const classNames </span>= require("./sidebar.css"<span style="color: rgba(0, 0, 0, 1)">);

interface Props {
isVisible: </span><span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)">;
}

const divStyle </span>= (props: Props): React.CSSProperties =&gt;<span style="color: rgba(0, 0, 0, 1)"> ({
width: props.isVisible </span>? "23rem" : "0rem"<span style="color: rgba(0, 0, 0, 1)">
});

export const SidebarComponent: React.StatelessComponent</span>&lt;Props&gt; = props =&gt;<span style="color: rgba(0, 0, 0, 1)"> (
</span>&lt;div id="mySidenav" className={classNames.sidenav} style={divStyle(props)}&gt;<span style="color: rgba(0, 0, 0, 1)">
    {props.children}
</span>&lt;/div&gt;
);</pre>
</div>
<p>这里divStyle组件的返回值就是React.CSSProperties类型。</p>
<p>我们还可以定义一个CSSProperties类型的变量:</p>
<div class="cnblogs_code">
<pre>const divStyle: React.CSSProperties =<span style="color: rgba(0, 0, 0, 1)"> {
    width: </span>"11rem"<span style="color: rgba(0, 0, 0, 1)">,
    height: </span>"7rem"<span style="color: rgba(0, 0, 0, 1)">,
    backgroundColor: `rgb(${props.color.red},${props.color.green}, ${props.color.blue})`
};</span></pre>
</div>
<p>这个变量可以在HTML标签的style属性上使用:</p>
<div class="cnblogs_code">
<pre>&lt;div style={divStyle} /&gt;</pre>
</div>
<p>在React的类型声明文件中,style属性的类型如下:</p>
<div class="cnblogs_code">
<pre>style?: CSSProperties | undefined;</pre>
</div>
<h2>三、React Hooks</h2>
<h3>1. useState</h3>
<p>默认情况下,React会为根据设置的state的初始值来自动推导state以及更新函数的类型:</p>
<p><img src="https://img2023.cnblogs.com/blog/1022151/202302/1022151-20230224161955198-967394410.png"></p>
<p>如果已知state 的类型,可以通过以下形式来自定义state的类型:</p>
<div class="cnblogs_code">
<pre>const = useState&lt;number&gt;(1)</pre>
</div>
<p>如果初始值为null,需要显式地声明 state 的类型:</p>
<div class="cnblogs_code">
<pre>const = useState&lt;number | <span style="color: rgba(0, 0, 255, 1)">null</span>&gt;(<span style="color: rgba(0, 0, 255, 1)">null</span>);</pre>
</div>
<p>如果state是一个对象,想要初始化一个空对象,可以使用断言来处理:</p>
<div class="cnblogs_code">
<pre>const = React.useState&lt;IUser&gt;({} as IUser);</pre>
</div>
<p>实际上,这里将空对象{}断言为IUser接口就是欺骗了TypeScript的编译器,由于后面的代码可能会依赖这个对象,所以应该在使用前及时初始化 user 的值,否则就会报错。</p>
<p>下面是声明文件中 useState 的定义:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">function</span> useState&lt;S&gt;(initialState: S | (() =&gt; S)): ;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> convenience overload when first argument is omitted</span>
<span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
* Returns a stateful value, and a function to update it.
   *
   * @version 16.8.0
   * @see https://reactjs.org/docs/hooks-reference.html#usestate
   </span><span style="color: rgba(0, 128, 0, 1)">*/</span>
   
<span style="color: rgba(0, 0, 255, 1)">function</span> useState&lt;S = undefined&gt;(): ;
</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
   * 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
   </span><span style="color: rgba(0, 128, 0, 1)">*/</span></pre>
</div>
<p>可以看到,这里定义两种形式,分别是有初始值和没有初始值的形式。</p>
<h3>2. useEffect</h3>
<p>useEffect的主要作用就是处理副作用,它的第一个参数是一个函数,表示要清除副作用的操作,第二个参数是一组值,当这组值改变时,第一个参数的函数才会执行,这让我们可以控制何时运行函数来处理副作用:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">useEffect(
() </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    const subscription </span>=<span style="color: rgba(0, 0, 0, 1)"> props.source.subscribe();
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> () =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
      subscription.unsubscribe();
    };
},

);</span></pre>
</div>
<p>当函数的返回值不是函数或者effect函数中未定义的内容时,如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">useEffect(
    () </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> {
      subscribe();
      </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">;
    }
);</span></pre>
</div>
<p>TypeScript就会报错:</p>
<p><img src="https://img2023.cnblogs.com/blog/1022151/202302/1022151-20230224162221471-963741673.png"></p>
<p>来看看useEffect在类型声明文件中的定义:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Destructors are only allowed to return void.</span>
type Destructor = () =&gt; <span style="color: rgba(0, 0, 255, 1)">void</span> |<span style="color: rgba(0, 0, 0, 1)"> { : never };

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> NOTE: callbacks are _only_ allowed to return either void, or a destructor.</span>
type EffectCallback = () =&gt; (<span style="color: rgba(0, 0, 255, 1)">void</span> |<span style="color: rgba(0, 0, 0, 1)"> Destructor);

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> TODO (TypeScript 3.0): ReadonlyArray&lt;unknown&gt;</span>
type DependencyList = ReadonlyArray&lt;any&gt;<span style="color: rgba(0, 0, 0, 1)">;

</span><span style="color: rgba(0, 0, 255, 1)">function</span> useEffect(effect: EffectCallback, deps?: DependencyList): <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> NOTE: this does not accept strings, but this will have to be fixed by removing strings from type Ref&lt;T&gt;</span>
<span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
   * `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
   </span><span style="color: rgba(0, 128, 0, 1)">*/</span></pre>
</div>
<p>可以看到,useEffect的第一个参数只允许返回一个函数。</p>
<h3>3. useRef</h3>
<p>当使用 useRef 时,我们可以访问一个可变的引用对象。可以将初始值传递给 useRef,它用于初始化可变 ref 对象公开的当前属性。当我们使用useRef时,需要给其指定类型:</p>
<div class="cnblogs_code">
<pre>const nameInput = React.useRef&lt;HTMLInputElement&gt;(<span style="color: rgba(0, 0, 255, 1)">null</span>)</pre>
</div>
<p>这里给实例的类型指定为了input输入框类型。</p>
<p>当useRef的初始值为null时,有两种创建的形式,第一种:</p>
<div class="cnblogs_code">
<pre>const nameInput = React.useRef&lt;HTMLInputElement&gt;(<span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">)
nameInput.current.innerText </span>= "hello world";</pre>
</div>
<p>这种形式下,ref1.current是只读的(read-only),所以当我们将它的innerText属性重新赋值时会报以下错误:</p>
<div class="cnblogs_code">
<pre>Cannot assign to 'current' because it is a read-only property.</pre>
</div>
<p>那该怎么将current属性变为动态可变的,先来看看类型声明文件中 useRef 是如何定义的:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">function</span> useRef&lt;T&gt;(initialValue: T): MutableRefObject&lt;T&gt;<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> convenience overload for refs given as a ref prop as they typically start with a null value</span>
<span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
   * `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
   </span><span style="color: rgba(0, 128, 0, 1)">*/</span></pre>
</div>
<p>这段代码的第十行的告诉我们,如果需要useRef的直接可变,就需要在泛型参数中包含'| null',所以这就是当初始值为null的第二种定义形式:</p>
<div class="cnblogs_code">
<pre>const nameInput = React.useRef&lt;HTMLInputElement | <span style="color: rgba(0, 0, 255, 1)">null</span>&gt;(<span style="color: rgba(0, 0, 255, 1)">null</span>);</pre>
</div>
<p>这种形式下,nameInput.current就是可写的。不过两种类型在使用时都需要做类型检查:</p>
<div class="cnblogs_code">
<pre>nameInput.current?.innerText = "hello world";</pre>
</div>
<p>那么问题来了,为什么第一种写法在没有操作current时没有报错呢?因为useRef在类型定义时具有多个重载声明,第一种方式就是执行的以下函数重载:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">function</span> useRef&lt;T&gt;(initialValue: T|<span style="color: rgba(0, 0, 255, 1)">null</span>): RefObject&lt;T&gt;<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> convenience overload for potentially undefined initialValue / call with 0 arguments</span><span style="color: rgba(0, 128, 0, 1)">
//</span><span style="color: rgba(0, 128, 0, 1)"> has a default to stop it from defaulting to {} instead</span><span style="color: rgba(0, 128, 0, 1)">
/*</span><span style="color: rgba(0, 128, 0, 1)">*
* `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
</span><span style="color: rgba(0, 128, 0, 1)">*/</span></pre>
</div>
<p>从上useRef的声明中可以看到,function useRef的返回值类型化是MutableRefObject,这里面的T就是参数的类型T,所以最终nameInput 的类型就是React.MutableRefObject。</p>
<p>注意,上面用到了HTMLInputElement类型,这是一个标签类型,这个操作就是用来访问DOM元素的。</p>
<h3>4. useCallback</h3>
<p>先来看看类型声明文件中对useCallback的定义:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">function</span> useCallback&lt;T extends (...args: any[]) =&gt; any&gt;<span style="color: rgba(0, 0, 0, 1)">(callback: T, deps: DependencyList): T;
</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
* `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, )
*   return ...
* }
* ```
*
* @version 16.8.0
* @see https://reactjs.org/docs/hooks-reference.html#usememo
</span><span style="color: rgba(0, 128, 0, 1)">*/</span></pre>
</div>
<p>useCallback接收一个回调函数和一个依赖数组,只有当依赖数组中的值发生变化时才会重新执行回调函数。来看一个例子:</p>
<div class="cnblogs_code">
<pre>const add = (a: number, b: number) =&gt; a +<span style="color: rgba(0, 0, 0, 1)"> b;

const memoizedCallback </span>=<span style="color: rgba(0, 0, 0, 1)"> useCallback(
(a) </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    add(a, b);
},

);</span></pre>
</div>
<p>这里我们没有给回调函数中的参数a定义类型,所以下面的调用方式都不会报错:</p>
<div class="cnblogs_code">
<pre>memoizedCallback("hello"<span style="color: rgba(0, 0, 0, 1)">);
memoizedCallback(</span>5)</pre>
</div>
<p>尽管add方法的两个参数都是number类型,但是上述调用都能够用执行。所以为了更加严谨,我们需要给回调函数定义具体的类型:</p>
<div class="cnblogs_code">
<pre>const memoizedCallback =<span style="color: rgba(0, 0, 0, 1)"> useCallback(
(a: number) </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    add(a, b);
},

);</span></pre>
</div>
<p>这时候如果再给回调函数传入字符串就会报错了:</p>
<div><img src="https://img2023.cnblogs.com/blog/1022151/202302/1022151-20230224162643192-136862297.png">
<p>所有,需要注意,在使用useCallback时需要给回调函数的参数指定类型。</p>
<h3>5. useMemo</h3>
<p>先来看看类型声明文件中对useMemo的定义:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">function</span> useMemo&lt;T&gt;(factory: () =&gt; T, deps: DependencyList |<span style="color: rgba(0, 0, 0, 1)"> undefined): T;
   </span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
    * `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
    </span><span style="color: rgba(0, 128, 0, 1)">*/</span></pre>
</div>
<p>useMemo和useCallback是非常类似的,但是它返回的是一个值,而不是函数。所以在定义useMemo时需要定义返回值的类型:</p>
<div class="cnblogs_code">
<pre>let a = 1<span style="color: rgba(0, 0, 0, 1)">;
setTimeout(() </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> {
a </span>+= 1<span style="color: rgba(0, 0, 0, 1)">;
}, </span>1000<span style="color: rgba(0, 0, 0, 1)">);

const calculatedValue </span>= useMemo&lt;number&gt;(() =&gt; a ** 2, );</pre>
</div>
<p>如果返回值不一致,就会报错:</p>
<div class="cnblogs_code">
<pre>const calculatedValue = useMemo&lt;number&gt;(() =&gt; a + "hello"<span style="color: rgba(0, 0, 0, 1)">, );
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 类型“() =&gt; string”的参数不能赋给类型“() =&gt; number”的参数</span></pre>
</div>
<h3>6. useContext</h3>
<p>useContext需要提供一个上下文对象,并返回所提供的上下文的值,当提供者更新上下文对象时,引用这些上下文对象的组件就会重新渲染:</p>
<div class="cnblogs_code">
<pre>const ColorContext = React.createContext({ color: "green"<span style="color: rgba(0, 0, 0, 1)"> });

const Welcome </span>= () =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
const { color } </span>=<span style="color: rgba(0, 0, 0, 1)"> useContext(ColorContext);
</span><span style="color: rgba(0, 0, 255, 1)">return</span> &lt;div style={{ color }}&gt;hello world&lt;/div&gt;;
};</pre>
</div>
<p>在使用useContext时,会自动推断出提供的上下文对象的类型,所以并不需要我们手动设置context的类型。当前,我们也可以使用泛型来设置context的类型:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">interface IColor {
color: string;
}

const ColorContext </span>= React.createContext&lt;IColor&gt;({ color: "green" });</pre>
</div>
<p>下面是useContext在类型声明文件中的定义:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">function</span> useContext&lt;T&gt;(context: Context&lt;T&gt;<span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">, (not public API) observedBits?: number|boolean </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">): T;
</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
* Returns a stateful value, and a function to update it.
*
* @version 16.8.0
* @see https://reactjs.org/docs/hooks-reference.html#usestate
</span><span style="color: rgba(0, 128, 0, 1)">*/</span></pre>
</div>
<h3>7. useReducer</h3>
<p>有时我们需要处理一些复杂的状态,并且可能取决于之前的状态。这时候就可以使用useReducer,它接收一个函数,这个函数会根据之前的状态来计算一个新的state。其语法如下:</p>
<div class="cnblogs_code">
<pre>const = useReducer(reducer, initialArg, init);</pre>
</div>
<p>来看下面的例子:</p>
<div class="cnblogs_code">
<pre>const reducer = (state, action) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">switch</span><span style="color: rgba(0, 0, 0, 1)"> (action.type) {
    </span><span style="color: rgba(0, 0, 255, 1)">case</span> 'increment'<span style="color: rgba(0, 0, 0, 1)">:
      </span><span style="color: rgba(0, 0, 255, 1)">return</span> {count: state.count + 1<span style="color: rgba(0, 0, 0, 1)">};
    </span><span style="color: rgba(0, 0, 255, 1)">case</span> 'decrement'<span style="color: rgba(0, 0, 0, 1)">:
      </span><span style="color: rgba(0, 0, 255, 1)">return</span> {count: state.count - 1<span style="color: rgba(0, 0, 0, 1)">};
    </span><span style="color: rgba(0, 0, 255, 1)">default</span><span style="color: rgba(0, 0, 0, 1)">:
      </span><span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Error();
}
}

const Counter </span>= () =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
const initialState </span>= {count: 0<span style="color: rgba(0, 0, 0, 1)">}
const </span>=<span style="color: rgba(0, 0, 0, 1)"> useReducer(reducer, initialState);

</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
    </span>&lt;&gt;<span style="color: rgba(0, 0, 0, 1)">
      Count: {state.count}
      </span>&lt;button onClick={() =&gt; dispatch({type: 'increment'})}&gt;+&lt;/button&gt;
      &lt;button onClick={() =&gt; dispatch({type: 'decrement'})}&gt;-&lt;/button&gt;
    &lt;/&gt;
<span style="color: rgba(0, 0, 0, 1)">);
}</span></pre>
</div>
<p>当前的状态是无法推断出来的,可以给reducer函数添加类型,通过给reducer函数定义state和action来推断 useReducer 的类型,下面来修改上面的例子:</p>
<div class="cnblogs_code">
<pre>type ActionType =<span style="color: rgba(0, 0, 0, 1)"> {
type: </span>'increment' | 'decrement'<span style="color: rgba(0, 0, 0, 1)">;
};

type State </span>=<span style="color: rgba(0, 0, 0, 1)"> { count: number };

const initialState: State </span>= {count: 0<span style="color: rgba(0, 0, 0, 1)">}
const reducer </span>= (state: State, action: ActionType) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ...</span>
}</pre>
</div>
<p>这样,在Counter函数中就可以推断出类型。当我们试图使用一个不存在的类型时,就会报错:</p>
<div class="cnblogs_code">
<pre>dispatch({type: 'reset'<span style="color: rgba(0, 0, 0, 1)">});
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Error! type '"reset"' is not assignable to type '"increment" | "decrement"'</span></pre>
</div>
<p>除此之外,还可以使用泛型的形式来实现reducer函数的类型定义:</p>
<div class="cnblogs_code">
<pre>type ActionType =<span style="color: rgba(0, 0, 0, 1)"> {
type: </span>'increment' | 'decrement'<span style="color: rgba(0, 0, 0, 1)">;
};

type State </span>=<span style="color: rgba(0, 0, 0, 1)"> { count: number };

const reducer: React.Reducer</span>&lt;State, ActionType&gt; = (state, action) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ...</span>
}</pre>
</div>
<p>其实dispatch方法也是有类型的:</p>
<div><img src="https://img2023.cnblogs.com/blog/1022151/202302/1022151-20230224163015015-1714193243.png">
<p>可以看到,dispatch的类型是:React.Dispatch,上面示例的完整代码如下:</p>
<div class="cnblogs_code">
<pre>import React, { useReducer } from "react"<span style="color: rgba(0, 0, 0, 1)">;

type ActionType </span>=<span style="color: rgba(0, 0, 0, 1)"> {
type: </span>"increment" | "decrement"<span style="color: rgba(0, 0, 0, 1)">;
};

type State </span>=<span style="color: rgba(0, 0, 0, 1)"> { count: number };

const Counter: React.FC </span>= () =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
const reducer: React.Reducer</span>&lt;State, ActionType&gt; = (state, action) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    </span><span style="color: rgba(0, 0, 255, 1)">switch</span><span style="color: rgba(0, 0, 0, 1)"> (action.type) {
      </span><span style="color: rgba(0, 0, 255, 1)">case</span> "increment"<span style="color: rgba(0, 0, 0, 1)">:
      </span><span style="color: rgba(0, 0, 255, 1)">return</span> { count: state.count + 1<span style="color: rgba(0, 0, 0, 1)"> };
      </span><span style="color: rgba(0, 0, 255, 1)">case</span> "decrement"<span style="color: rgba(0, 0, 0, 1)">:
      </span><span style="color: rgba(0, 0, 255, 1)">return</span> { count: state.count - 1<span style="color: rgba(0, 0, 0, 1)"> };
      </span><span style="color: rgba(0, 0, 255, 1)">default</span><span style="color: rgba(0, 0, 0, 1)">:
      </span><span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Error();
    }
};

const initialState: State </span>= {count: 0<span style="color: rgba(0, 0, 0, 1)">}
const </span>=<span style="color: rgba(0, 0, 0, 1)"> useReducer(reducer, initialState);

</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
    </span>&lt;&gt;<span style="color: rgba(0, 0, 0, 1)">
      Count: {state.count}
      </span>&lt;button onClick={() =&gt; dispatch({ type: "increment" })}&gt;+&lt;/button&gt;
      &lt;button onClick={() =&gt; dispatch({ type: "decrement" })}&gt;-&lt;/button&gt;
    &lt;/&gt;
<span style="color: rgba(0, 0, 0, 1)">);
};

export </span><span style="color: rgba(0, 0, 255, 1)">default</span> Counter;</pre>
</div>
<h2>四、事件处理</h2>
<h3>1. Event 事件类型</h3>
<p>在开发中我们会经常在事件处理函数中使用event事件对象,比如在input框输入时实时获取输入的值;使用鼠标事件时,通过 clientX、clientY 获取当前指针的坐标等等。</p>
<p>我们知道,Event是一个对象,并且有很多属性,这时很多人就会把 event 类型定义为any,这样的话TypeScript就失去了它的意义,并不会对event事件进行静态检查,如果一个键盘事件触发了下面的方法,也不会报错:</p>
<div class="cnblogs_code">
<pre>const handleEvent = (e: any) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    console.log(e.clientX, e.clientY)
}</span></pre>
</div>
<p>由于Event事件对象中有很多的属性,所以我们也不方便把所有属性及其类型定义在一个interface中,所以React在声明文件中给我们提供了Event事件对象的类型声明。</p>
<p>常见的Event 事件对象如下:</p>
<ul>
<li>
<p>剪切板事件对象:ClipboardEvent&lt;T = Element&gt;</p>
</li>
<li>
<p>拖拽事件对象:DragEvent&lt;T = Element&gt;</p>
</li>
<li>
<p>焦点事件对象:FocusEvent&lt;T = Element&gt;</p>
</li>
<li>
<p>表单事件对象:FormEvent&lt;T = Element&gt;</p>
</li>
<li>
<p>Change事件对象:ChangeEvent&lt;T = Element&gt;</p>
</li>
<li>
<p>键盘事件对象:KeyboardEvent&lt;T = Element&gt;</p>
</li>
<li>
<p>鼠标事件对象:MouseEvent&lt;T = Element, E = NativeMouseEvent&gt;</p>
</li>
<li>
<p>触摸事件对象:TouchEvent&lt;T = Element&gt;</p>
</li>
<li>
<p>滚轮事件对象:WheelEvent&lt;T = Element&gt;</p>
</li>
<li>
<p>动画事件对象:AnimationEvent&lt;T = Element&gt;</p>
</li>
<li>
<p>过渡事件对象:TransitionEvent&lt;T = Element&gt;</p>
</li>
</ul>
<p>可以看到,这些Event事件对象的泛型中都会接收一个Element元素的类型,这个类型就是我们绑定这个事件的标签元素的类型,标签元素类型将在下面的第五部分介绍。</p>
<p>来看一个简单的例子:</p>
<div class="cnblogs_code">
<pre>type State =<span style="color: rgba(0, 0, 0, 1)"> {
text: string;
};

const App: React.FC </span>= () =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
const </span>= useState&lt;string&gt;(""<span style="color: rgba(0, 0, 0, 1)">)

const onChange </span>= (e: React.FormEvent&lt;HTMLInputElement&gt;): <span style="color: rgba(0, 0, 255, 1)">void</span> =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    setText(e.currentTarget.value);
};

</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
    </span>&lt;div&gt;
      &lt;input type="text" value={text} onChange={onChange} /&gt;
    &lt;/div&gt;
<span style="color: rgba(0, 0, 0, 1)">);
}</span></pre>
</div>
<p>这里就给onChange方法的事件对象定义为了FormEvent类型,并且作用的对象是一个HTMLInputElement类型的标签(input标签)</p>
<p>可以来看下MouseEvent事件对象和ChangeEvent事件对象的类型声明,其他事件对象的声明形似也类似:</p>
<div class="cnblogs_code">
<pre>interface MouseEvent&lt;T = Element, E = NativeMouseEvent&gt; extends UIEvent&lt;T, E&gt;<span style="color: rgba(0, 0, 0, 1)"> {
altKey: </span><span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)">;
button: number;
buttons: number;
clientX: number;
clientY: number;
ctrlKey: </span><span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
    * See (https://www.w3.org/TR/uievents-key/#keys-modifier). for a list of valid (case-sensitive) arguments to this method.
    </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
getModifierState(key: string): </span><span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)">;
metaKey: </span><span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)">;
movementX: number;
movementY: number;
pageX: number;
pageY: number;
relatedTarget: EventTarget </span>| <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">;
screenX: number;
screenY: number;
shiftKey: </span><span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)">;
}

interface ChangeEvent</span>&lt;T = Element&gt; extends SyntheticEvent&lt;T&gt;<span style="color: rgba(0, 0, 0, 1)"> {
target: EventTarget </span>&amp;<span style="color: rgba(0, 0, 0, 1)"> T;
}</span></pre>
</div>
<p>在很多事件对象的声明文件中都可以看到 EventTarget 的身影。这是因为,DOM的事件操作(监听和触发),都定义在EventTarget接口上。EventTarget 的类型声明如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">interface EventTarget {
    addEventListener(type: string, listener: EventListenerOrEventListenerObject </span>| <span style="color: rgba(0, 0, 255, 1)">null</span>, options?: <span style="color: rgba(0, 0, 255, 1)">boolean</span> | AddEventListenerOptions): <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)">;
    dispatchEvent(evt: Event): </span><span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)">;
    removeEventListener(type: string, listener</span>?: EventListenerOrEventListenerObject | <span style="color: rgba(0, 0, 255, 1)">null</span>, options?: EventListenerOptions | <span style="color: rgba(0, 0, 255, 1)">boolean</span>): <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)">;
}</span></pre>
</div>
<p>比如在change事件中,会使用的e.target来获取当前的值,它的的类型就是EventTarget。来看下面的例子:</p>
<div class="cnblogs_code">
<pre>&lt;<span style="color: rgba(0, 0, 0, 1)">input
onChange</span>={e =&gt;<span style="color: rgba(0, 0, 0, 1)"> onSourceChange(e)}
placeholder</span>="最多30个字"
/&gt;
<span style="color: rgba(0, 0, 0, 1)">
const onSourceChange </span>= (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (e.target.value.length &gt; 30<span style="color: rgba(0, 0, 0, 1)">) {
      message.error(</span>'请长度不能超过30个字,请重新输入'<span style="color: rgba(0, 0, 0, 1)">);
      </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
    }
    setSourceInput(e.target.value);
};</span></pre>
</div>
<p>这里定义了一个input输入框,当触发onChange事件时,会调用onSourceChange方法,该方法的参数e的类型就是:React.ChangeEvent,而e.target的类型就是EventTarget:</p>
<p><img src="https://img2023.cnblogs.com/blog/1022151/202302/1022151-20230224163226606-189604263.png"></p>
<p>再来看一个例子:</p>
<div class="cnblogs_code">
<pre>questionList.map(item =&gt;<span style="color: rgba(0, 0, 0, 1)"> (
    </span>&lt;<span style="color: rgba(0, 0, 0, 1)">div
   key</span>=<span style="color: rgba(0, 0, 0, 1)">{item.id}
   role</span>="button"<span style="color: rgba(0, 0, 0, 1)">
   onClick</span>={e =&gt;<span style="color: rgba(0, 0, 0, 1)"> handleChangeCurrent(item, e)}
    </span>&gt;
    <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 组件内容...</span>
    &lt;/div&gt;
<span style="color: rgba(0, 0, 0, 1)">)

const handleChangeCurrent </span>= (item: IData, e: React.MouseEvent&lt;HTMLDivElement&gt;) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    e.stopPropagation();
    setCurrent(item);
};</span></pre>
</div>
<p>这点代码中,点击某个盒子,就将它设置为当前的盒子,方便执行其他操作。当鼠标点击盒子时,会触发handleChangeCurren方法,该方法有两个参数,第二个参数是event对象,在方法中执行了e.stopPropagation();是为了阻止冒泡事件,这里的stopPropagation()实际上并不是鼠标事件MouseEvent的属性,它是合成事件上的属性,来看看声明文件中的定义:</p>
<div class="cnblogs_code">
<pre>interface MouseEvent&lt;T = Element, E = NativeMouseEvent&gt; extends UIEvent&lt;T, E&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">...   </span>
<span style="color: rgba(0, 0, 0, 1)">}

interface UIEvent</span>&lt;T = Element, E = NativeUIEvent&gt; extends SyntheticEvent&lt;T, E&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">...</span>
<span style="color: rgba(0, 0, 0, 1)">}

interface SyntheticEvent</span>&lt;T = Element, E = Event&gt; extends BaseSyntheticEvent&lt;E, EventTarget &amp; T, EventTarget&gt;<span style="color: rgba(0, 0, 0, 1)"> {}

interface BaseSyntheticEvent</span>&lt;E = object, C = any, T = any&gt;<span style="color: rgba(0, 0, 0, 1)"> {
nativeEvent: E;
currentTarget: C;
target: T;
bubbles: </span><span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)">;
cancelable: </span><span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)">;
defaultPrevented: </span><span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)">;
eventPhase: number;
isTrusted: </span><span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)">;
preventDefault(): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)">;
isDefaultPrevented(): </span><span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)">;
stopPropagation(): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)">;
isPropagationStopped(): </span><span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)">;
persist(): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)">;
timeStamp: number;
type: string;
}</span></pre>
</div>
<p>可以看到,这里的stopPropagation()是一层层的继承来的,最终来自于BaseSyntheticEvent合成事件类型。原生的事件集合SyntheticEvent就是继承自合成时间类型。SyntheticEvent&lt;T = Element, E = Event&gt;泛型接口接收当前的元素类型和事件类型,如果不介意这两个参数的类型,完全可以这样写:</p>
<div class="cnblogs_code">
<pre>&lt;<span style="color: rgba(0, 0, 0, 1)">input
onChange</span>={(e: SyntheticEvent&lt;Element, Event&gt;)=&gt;<span style="color: rgba(0, 0, 0, 1)">{
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">... </span>
<span style="color: rgba(0, 0, 0, 1)">}}
</span>/&gt;</pre>
</div>
<h3>2. 事件处理函数类型</h3>
<p>说完事件对象类型,再来看看事件处理函数的类型。React也为我们提供了贴心的提供了事件处理函数的类型声明,来看看所有的事件处理函数的类型声明:</p>
<div class="cnblogs_code">
<pre>type EventHandler&lt;E extends SyntheticEvent&lt;any&gt;&gt; = { bivarianceHack(event: E): <span style="color: rgba(0, 0, 255, 1)">void</span> }["bivarianceHack"<span style="color: rgba(0, 0, 0, 1)">];

type ReactEventHandler</span>&lt;T = Element&gt; = EventHandler&lt;SyntheticEvent&lt;T&gt;&gt;<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 剪切板事件处理函数</span>
type ClipboardEventHandler&lt;T = Element&gt; = EventHandler&lt;ClipboardEvent&lt;T&gt;&gt;<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 复合事件处理函数</span>
type CompositionEventHandler&lt;T = Element&gt; = EventHandler&lt;CompositionEvent&lt;T&gt;&gt;<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 拖拽事件处理函数</span>
type DragEventHandler&lt;T = Element&gt; = EventHandler&lt;DragEvent&lt;T&gt;&gt;<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 焦点事件处理函数</span>
type FocusEventHandler&lt;T = Element&gt; = EventHandler&lt;FocusEvent&lt;T&gt;&gt;<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 表单事件处理函数</span>
type FormEventHandler&lt;T = Element&gt; = EventHandler&lt;FormEvent&lt;T&gt;&gt;<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Change事件处理函数</span>
type ChangeEventHandler&lt;T = Element&gt; = EventHandler&lt;ChangeEvent&lt;T&gt;&gt;<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 键盘事件处理函数</span>
type KeyboardEventHandler&lt;T = Element&gt; = EventHandler&lt;KeyboardEvent&lt;T&gt;&gt;<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 鼠标事件处理函数</span>
type MouseEventHandler&lt;T = Element&gt; = EventHandler&lt;MouseEvent&lt;T&gt;&gt;<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 触屏事件处理函数</span>
type TouchEventHandler&lt;T = Element&gt; = EventHandler&lt;TouchEvent&lt;T&gt;&gt;<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 指针事件处理函数</span>
type PointerEventHandler&lt;T = Element&gt; = EventHandler&lt;PointerEvent&lt;T&gt;&gt;<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 界面事件处理函数</span>
type UIEventHandler&lt;T = Element&gt; = EventHandler&lt;UIEvent&lt;T&gt;&gt;<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 滚轮事件处理函数</span>
type WheelEventHandler&lt;T = Element&gt; = EventHandler&lt;WheelEvent&lt;T&gt;&gt;<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 动画事件处理函数</span>
type AnimationEventHandler&lt;T = Element&gt; = EventHandler&lt;AnimationEvent&lt;T&gt;&gt;<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 过渡事件处理函数</span>
type TransitionEventHandler&lt;T = Element&gt; = EventHandler&lt;TransitionEvent&lt;T&gt;&gt;;</pre>
</div>
<p>这里面的T的类型也都是Element,指的是触发该事件的HTML标签元素的类型,下面第五部分会介绍。</p>
<p>EventHandler会接收一个E,它表示事件处理函数中 Event 对象的类型。bivarianceHack 是事件处理函数的类型定义,函数接收一个 Event 对象,并且其类型为接收到的泛型变量 E 的类型, 返回值为 void。</p>
<p>还看上面的那个例子:</p>
<div class="cnblogs_code">
<pre>type State =<span style="color: rgba(0, 0, 0, 1)"> {
text: string;
};

const App: React.FC </span>= () =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
const </span>= useState&lt;string&gt;(""<span style="color: rgba(0, 0, 0, 1)">)

const onChange: React.ChangeEventHandler</span>&lt;HTMLInputElement&gt; = (e) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    setText(e.currentTarget.value);
};

</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
    </span>&lt;div&gt;
      &lt;input type="text" value={text} onChange={onChange} /&gt;
    &lt;/div&gt;
<span style="color: rgba(0, 0, 0, 1)">);
}</span></pre>
</div>
<p>这里给onChange方法定义了方法的类型,它是一个ChangeEventHandler的类型,并且作用的对象是一个HTMLImnputElement类型的标签(input标签)。</p>
<h2>五、HTML标签类型</h2>
<h3>1. 常见标签类型</h3>
<p>在项目的依赖文件中可以找到HTML标签相关的类型声明文件:</p>
<p><img src="https://img2023.cnblogs.com/blog/1022151/202302/1022151-20230224163412963-2103945867.png"></p>
<p>所有的HTML标签的类型都被定义在 intrinsicElements 接口中,常见的标签及其类型如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">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;</span></pre>
</div>
<p>那什么时候会使用到标签类型呢,上面第四部分的Event事件类型和事件处理函数类型中都使用到了标签的类型。上面的很多的类型都需要传入一个ELement类型的泛型参数,这个泛型参数就是对应的标签类型值,可以根据标签来选择对应的标签类型。这些类型都继承自HTMLElement类型,如果使用时对类型类型要求不高,可以直接写HTMLELement。比如下面的例子:</p>
<div class="cnblogs_code">
<pre>&lt;<span style="color: rgba(0, 0, 0, 1)">Button
type</span>="text"<span style="color: rgba(0, 0, 0, 1)">
onClick</span>={(e: React.MouseEvent&lt;HTMLElement&gt;) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
handleOperate();
e.stopPropagation();
}}
</span>&gt;
    &lt;<span style="color: rgba(0, 0, 0, 1)">img
src</span>=<span style="color: rgba(0, 0, 0, 1)">{cancelChangeIcon}
alt</span>=""
    /&gt;
<span style="color: rgba(0, 0, 0, 1)">    取消修改
</span>&lt;/Button&gt;</pre>
</div>
<p>其实,在直接操作DOM时也会用到标签类型,虽然我们现在通常会使用框架来开发,但是有时候也避免不了直接操作DOM。比如我在工作中,项目中的某一部分组件是通过npm来引入的其他组的组件,而在很多时候,我有需要动态的去个性化这个组件的样式,最直接的办法就是通过原生JavaScript获取到DOM元素,来进行样式的修改,这时候就会用到标签类型。</p>
<p>来看下面的例子:</p>
<div class="cnblogs_code">
<pre>document.querySelectorAll('.paper').forEach(item =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
const firstPageHasAddEle </span>= (item.firstChild as HTMLDivElement).classList.contains('add-ele'<span style="color: rgba(0, 0, 0, 1)">);

</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (firstPageHasAddEle) {
    item.removeChild(item.firstChild as ChildNode);
}
})</span></pre>
</div>
<p>这是我最近写的一段代码(略微删改),在第一页有个add-ele元素的时候就删除它。这里我们将item.firstChild断言成了HTMLDivElement类型,如果不断言,item.firstChild的类型就是ChildNode,而ChildNode类型中是不存在classList属性的,所以就就会报错,当我们把他断言成HTMLDivElement类型时,就不会报错了。很多时候,标签类型可以和断言(as)一起使用。</p>
<p>后面在removeChild时又使用了as断言,为什么呢?item.firstChild不是已经自动识别为ChildNode类型了吗?因为TS会认为,我们可能不能获取到类名为paper的元素,所以item.firstChild的类型就被推断为ChildNode | null,我们有时候比TS更懂我们定义的元素,知道页面一定存在paper 元素,所以可以直接将item.firstChild断言成ChildNode类型。</p>
<h3>2. 标签属性类型</h3>
<p>众所周知,每个HTML标签都有自己的属性,比如Input框就有value、width、placeholder、max-length等属性,下面是Input框的属性类型定义:</p>
<div class="cnblogs_code">
<pre>interface InputHTMLAttributes&lt;T&gt; extends HTMLAttributes&lt;T&gt;<span style="color: rgba(0, 0, 0, 1)"> {
accept</span>?: string |<span style="color: rgba(0, 0, 0, 1)"> undefined;
alt</span>?: string |<span style="color: rgba(0, 0, 0, 1)"> undefined;
autoComplete</span>?: string |<span style="color: rgba(0, 0, 0, 1)"> undefined;
autoFocus</span>?: <span style="color: rgba(0, 0, 255, 1)">boolean</span> |<span style="color: rgba(0, 0, 0, 1)"> undefined;
capture</span>?: <span style="color: rgba(0, 0, 255, 1)">boolean</span> | string |<span style="color: rgba(0, 0, 0, 1)"> undefined;
checked</span>?: <span style="color: rgba(0, 0, 255, 1)">boolean</span> |<span style="color: rgba(0, 0, 0, 1)"> undefined;
crossOrigin</span>?: string |<span style="color: rgba(0, 0, 0, 1)"> undefined;
disabled</span>?: <span style="color: rgba(0, 0, 255, 1)">boolean</span> |<span style="color: rgba(0, 0, 0, 1)"> undefined;
enterKeyHint</span>?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send' |<span style="color: rgba(0, 0, 0, 1)"> undefined;
form</span>?: string |<span style="color: rgba(0, 0, 0, 1)"> undefined;
formAction</span>?: string |<span style="color: rgba(0, 0, 0, 1)"> undefined;
formEncType</span>?: string |<span style="color: rgba(0, 0, 0, 1)"> undefined;
formMethod</span>?: string |<span style="color: rgba(0, 0, 0, 1)"> undefined;
formNoValidate</span>?: <span style="color: rgba(0, 0, 255, 1)">boolean</span> |<span style="color: rgba(0, 0, 0, 1)"> undefined;
formTarget</span>?: string |<span style="color: rgba(0, 0, 0, 1)"> undefined;
height</span>?: number | string |<span style="color: rgba(0, 0, 0, 1)"> undefined;
list</span>?: string |<span style="color: rgba(0, 0, 0, 1)"> undefined;
max</span>?: number | string |<span style="color: rgba(0, 0, 0, 1)"> undefined;
maxLength</span>?: number |<span style="color: rgba(0, 0, 0, 1)"> undefined;
min</span>?: number | string |<span style="color: rgba(0, 0, 0, 1)"> undefined;
minLength</span>?: number |<span style="color: rgba(0, 0, 0, 1)"> undefined;
multiple</span>?: <span style="color: rgba(0, 0, 255, 1)">boolean</span> |<span style="color: rgba(0, 0, 0, 1)"> undefined;
name</span>?: string |<span style="color: rgba(0, 0, 0, 1)"> undefined;
pattern</span>?: string |<span style="color: rgba(0, 0, 0, 1)"> undefined;
placeholder</span>?: string |<span style="color: rgba(0, 0, 0, 1)"> undefined;
readOnly</span>?: <span style="color: rgba(0, 0, 255, 1)">boolean</span> |<span style="color: rgba(0, 0, 0, 1)"> undefined;
required</span>?: <span style="color: rgba(0, 0, 255, 1)">boolean</span> |<span style="color: rgba(0, 0, 0, 1)"> undefined;
size</span>?: number |<span style="color: rgba(0, 0, 0, 1)"> undefined;
src</span>?: string |<span style="color: rgba(0, 0, 0, 1)"> undefined;
step</span>?: number | string |<span style="color: rgba(0, 0, 0, 1)"> undefined;
type</span>?: string |<span style="color: rgba(0, 0, 0, 1)"> undefined;
value</span>?: string | ReadonlyArray&lt;string&gt; | number |<span style="color: rgba(0, 0, 0, 1)"> undefined;
width</span>?: number | string |<span style="color: rgba(0, 0, 0, 1)"> undefined;

onChange</span>?: ChangeEventHandler&lt;T&gt; |<span style="color: rgba(0, 0, 0, 1)"> undefined;
}</span></pre>
</div>
<p>如果我们需要直接操作DOM,就可能会用到元素属性类型,常见的元素属性类型如下:</p>
<ul>
<li>
<p>HTML属性类型:HTMLAttributes</p>
</li>
<li>
<p>按钮属性类型:ButtonHTMLAttributes</p>
</li>
<li>
<p>表单属性类型:FormHTMLAttributes</p>
</li>
<li>
<p>图片属性类型:ImgHTMLAttributes</p>
</li>
<li>
<p>输入框属性类型:InputHTMLAttributes</p>
</li>
<li>
<p>链接属性类型:LinkHTMLAttributes</p>
</li>
<li>
<p>meta属性类型:MetaHTMLAttributes</p>
</li>
<li>
<p>选择框属性类型:SelectHTMLAttributes</p>
</li>
<li>
<p>表格属性类型:TableHTMLAttributes</p>
</li>
<li>
<p>输入区属性类型:TextareaHTMLAttributes</p>
</li>
<li>
<p>视频属性类型:VideoHTMLAttributes</p>
</li>
<li>
<p>SVG属性类型:SVGAttributes</p>
</li>
<li>
<p>WebView属性类型:WebViewHTMLAttributes</p>
</li>
</ul>
<p>一般情况下,我们是很少需要在项目中显式的去定义标签属性的类型。如果子级去封装组件库的话,这些属性就能发挥它们的作用了。来看例子(来源于网络,仅供学习):</p>
<div class="cnblogs_code">
<pre>import React from 'react'<span style="color: rgba(0, 0, 0, 1)">;
import classNames from </span>'classnames'<span style="color: rgba(0, 0, 0, 1)">

export enum ButtonSize {
    Large </span>= 'lg'<span style="color: rgba(0, 0, 0, 1)">,
    Small </span>= 'sm'<span style="color: rgba(0, 0, 0, 1)">
}

export enum ButtonType {
    Primary </span>= 'primary'<span style="color: rgba(0, 0, 0, 1)">,
    Default </span>= 'default'<span style="color: rgba(0, 0, 0, 1)">,
    Danger </span>= 'danger'<span style="color: rgba(0, 0, 0, 1)">,
    Link </span>= 'link'<span style="color: rgba(0, 0, 0, 1)">
}

interface BaseButtonProps {
    className</span>?<span style="color: rgba(0, 0, 0, 1)">: string;
    disabled</span>?: <span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)">;
    size</span>?<span style="color: rgba(0, 0, 0, 1)">: ButtonSize;
    btnType</span>?<span style="color: rgba(0, 0, 0, 1)">: ButtonType;
    children: React.ReactNode;
    href</span>?<span style="color: rgba(0, 0, 0, 1)">: string;   
}

type NativeButtonProps </span>= BaseButtonProps &amp; React.ButtonHTMLAttributes&lt;HTMLButtonElement&gt; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 使用 交叉类型(&amp;) 获得我们自己定义的属性和原生 button 的属性</span>
type AnchorButtonProps = BaseButtonProps &amp; React.AnchorHTMLAttributes&lt;HTMLAnchorElement&gt; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 使用 交叉类型(&amp;) 获得我们自己定义的属性和原生 a标签 的属性</span>
<span style="color: rgba(0, 0, 0, 1)">
export type ButtonProps </span>= Partial&lt;NativeButtonProps &amp; AnchorButtonProps&gt; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">使用 Partial&lt;&gt; 使两种属性可选</span>
<span style="color: rgba(0, 0, 0, 1)">
const Button: React.FC</span>&lt;ButtonProps&gt; = (props) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    const {
      disabled,
      className,
      size,
      btnType,
      children,
      href,
      ...restProps
    } </span>=<span style="color: rgba(0, 0, 0, 1)"> props;

    const classes </span>= classNames('btn'<span style="color: rgba(0, 0, 0, 1)">, className, {
      [`btn</span>-<span style="color: rgba(0, 0, 0, 1)">${btnType}`]: btnType,
      [`btn</span>-<span style="color: rgba(0, 0, 0, 1)">${size}`]: size,
      </span>'disabled': (btnType === ButtonType.Link) &amp;&amp; disabled<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 只有 a 标签才有 disabled 类名,button没有</span>
<span style="color: rgba(0, 0, 0, 1)">    })

    </span><span style="color: rgba(0, 0, 255, 1)">if</span>(btnType === ButtonType.Link &amp;&amp;<span style="color: rgba(0, 0, 0, 1)"> href) {
      </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
            </span>&lt;<span style="color: rgba(0, 0, 0, 1)">a
             className</span>=<span style="color: rgba(0, 0, 0, 1)">{classes}
             href</span>=<span style="color: rgba(0, 0, 0, 1)">{href}
             {...restProps}
            </span>&gt;<span style="color: rgba(0, 0, 0, 1)">
                {children}
            </span>&lt;/a&gt;
<span style="color: rgba(0, 0, 0, 1)">      )

    } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
      </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
            </span>&lt;<span style="color: rgba(0, 0, 0, 1)">button
             className</span>=<span style="color: rgba(0, 0, 0, 1)">{classes}
             disabled</span>={disabled} <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> button元素默认有disabled属性,所以即便没给他设置样式也会和普通button有一定区别</span>
<span style="color: rgba(0, 0, 0, 1)">
             {...restProps}
            </span>&gt;<span style="color: rgba(0, 0, 0, 1)">
                {children}
            </span>&lt;/button&gt;
<span style="color: rgba(0, 0, 0, 1)">      )
    }
}

Button.defaultProps </span>=<span style="color: rgba(0, 0, 0, 1)"> {
    disabled: </span><span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">,
    btnType: ButtonType.Default
}

export </span><span style="color: rgba(0, 0, 255, 1)">default</span> Button;</pre>
</div>
<p>这段代码就是用来封装一个buttom按钮,在button的基础上添加了一些自定义属性,比如上面将button的类型使用交叉类型(&amp;)获得自定义属性和原生 button 属性 :</p>
<div class="cnblogs_code">
<pre>type NativeButtonProps = BaseButtonProps &amp; React.ButtonHTMLAttributes&lt;HTMLButtonElement&gt;</pre>
</div>
<p>可以看到,标签属性类型在封装组件库时还是很有用的,更多用途可以自己探索~</p>
<h2>六、工具泛型</h2>
<p>在项目中使用一些工具泛型可以提高我们的开发效率,少写很多类型定义。下面来看看有哪些常见的工具泛型,以及其使用方式。</p>
<h3>1. Partial</h3>
<p>Partial 作用是将传入的属性变为可选项。适用于对类型结构不明确的情况。它使用了两个关键字:keyof和in,先来看看他们都是什么含义。keyof 可以用来取得接口的所有 key 值:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">interface IPerson {
name: string;
age: number;
height: number;
}
type T </span>=<span style="color: rgba(0, 0, 0, 1)"> keyof IPerson
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> T 类型为: "name" | "age" | "number"</span></pre>
</div>
<p>in关键字可以遍历枚举类型,:</p>
<div class="cnblogs_code">
<pre>type Person = "name" | "age" | "number"<span style="color: rgba(0, 0, 0, 1)">
type Obj </span>=<span style="color: rgba(0, 0, 0, 1)">{
: any
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Obj类型为: { name: any, age: any, number: any }</span></pre>
</div>
<p>keyof 可以产生联合类型, in 可以遍历枚举类型, 所以经常一起使用, 下面是Partial工具泛型的定义:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
* Make all properties in T optional
* 将T中的所有属性设置为可选
</span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
type Partial</span>&lt;T&gt; =<span style="color: rgba(0, 0, 0, 1)"> {
    ?<span style="color: rgba(0, 0, 0, 1)">: T;
};</span></pre>
</div>
<p>这里,keyof T 获取 T 所有属性名, 然后使用 in 进行遍历, 将值赋给 P, 最后 T 取得相应属性的值。中间的?就用来将属性设置为可选。</p>
<p>使用示例如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">interface IPerson {
name: string;
age: number;
height: number;
}

const person: Partial</span>&lt;IPerson&gt; =<span style="color: rgba(0, 0, 0, 1)"> {
name: </span>"zhangsan"<span style="color: rgba(0, 0, 0, 1)">;
}</span></pre>
</div>
<h3>2. Required</h3>
<p>Required 的作用是将传入的属性变为必选项,和上面的工具泛型恰好相反,其声明如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
* Make all properties in T required
* 将T中的所有属性设置为必选
</span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
type Required</span>&lt;T&gt; =<span style="color: rgba(0, 0, 0, 1)"> {
    -?<span style="color: rgba(0, 0, 0, 1)">: T;
};</span></pre>
</div>
<p>可以看到,这里使用-?将属性设置为必选,可以理解为减去问号。使用形式和上面的Partial差不多:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">interface IPerson {
name</span>?<span style="color: rgba(0, 0, 0, 1)">: string;
age</span>?<span style="color: rgba(0, 0, 0, 1)">: number;
height</span>?<span style="color: rgba(0, 0, 0, 1)">: number;
}

const person: Required</span>&lt;IPerson&gt; =<span style="color: rgba(0, 0, 0, 1)"> {
name: </span>"zhangsan"<span style="color: rgba(0, 0, 0, 1)">;
age: </span>18<span style="color: rgba(0, 0, 0, 1)">;
height: </span>180<span style="color: rgba(0, 0, 0, 1)">;
}</span></pre>
</div>
<h3>3. Readonly</h3>
<p>将T类型的所有属性设置为只读(readonly),构造出来类型的属性不能被再次赋值。Readonly的声明形式如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
* Make all properties in T readonly
</span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
type Readonly</span>&lt;T&gt; =<span style="color: rgba(0, 0, 0, 1)"> {
    readonly : T;
};</span></pre>
</div>
<p>使用示例如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">interface IPerson {
name: string;
age: number;
}

const person: Readonly</span>&lt;IPerson&gt; =<span style="color: rgba(0, 0, 0, 1)"> {
name: </span>"zhangsan"<span style="color: rgba(0, 0, 0, 1)">,
age: </span>18<span style="color: rgba(0, 0, 0, 1)">
}

person.age </span>= 20;<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">Error: cannot reassign a readonly property</span></pre>
</div>
<p>可以看到,通过 Readonly将IPerson的属性转化成了只读,不能再进行赋值操作。</p>
<h3>4. Pick&lt;T, K extends keyof T&gt;</h3>
<p>从T类型中挑选部分属性K来构造新的类型。它的声明形式如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
* From T, pick a set of properties whose keys are in the union K
</span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
type Pick</span>&lt;T, K extends keyof T&gt; =<span style="color: rgba(0, 0, 0, 1)"> {
    : T;
};</span></pre>
</div>
<p>使用示例如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">interface IPerson {
name: string;
age: number;
height: number;
}

const person: Pick</span>&lt;IPerson, "name" | "age"&gt; =<span style="color: rgba(0, 0, 0, 1)"> {
name: </span>"zhangsan"<span style="color: rgba(0, 0, 0, 1)">,
age: </span>18<span style="color: rgba(0, 0, 0, 1)">
}</span></pre>
</div>
<h3>5. Record&lt;K extends keyof any, T&gt;</h3>
<p>Record 用来构造一个类型,其属性名的类型为K,属性值的类型为T。这个工具泛型可用来将某个类型的属性映射到另一个类型上,下面是其声明形式:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
* Construct a type with a set of properties K of type T
</span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
type Record</span>&lt;K extends keyof any, T&gt; =<span style="color: rgba(0, 0, 0, 1)"> {
    : T;
};</span></pre>
</div>
<p>使用示例如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">interface IPageinfo {
    title: string;
}

type IPage </span>= 'home' | 'about' | 'contact'<span style="color: rgba(0, 0, 0, 1)">;

const page: Record</span>&lt;IPage, IPageinfo&gt; =<span style="color: rgba(0, 0, 0, 1)"> {
    about: {title: </span>'about'<span style="color: rgba(0, 0, 0, 1)">},
    contact: {title: </span>'contact'<span style="color: rgba(0, 0, 0, 1)">},
    home: {title: </span>'home'<span style="color: rgba(0, 0, 0, 1)">},
}</span></pre>
</div>
<h3>6. Exclude&lt;T, U&gt;</h3>
<p>Exclude 就是从一个联合类型中排除掉属于另一个联合类型的子集,下面是其声明的形式:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
* Exclude from T those types that are assignable to U
</span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
type Exclude</span>&lt;T, U&gt; = T extends U ? never : T;</pre>
</div>
<p>使用示例如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">interface IPerson {
name: string;
age: number;
height: number;
}

const person: Exclude</span>&lt;IPerson, "age" | "sex"&gt; =<span style="color: rgba(0, 0, 0, 1)"> {
name: </span>"zhangsan"<span style="color: rgba(0, 0, 0, 1)">;
height: </span>180<span style="color: rgba(0, 0, 0, 1)">;
}</span></pre>
</div>
<h3>7. Omit&lt;T, K extends keyof any&gt;</h3>
<p>上面的Pick 和 Exclude 都是最基础基础的工具泛型,很多时候用 Pick 或者 Exclude 还不如直接写类型更直接。而 Omit 就基于这两个来做的一个更抽象的封装,它允许从一个对象中剔除若干个属性,剩下的就是需要的新类型。下面是它的声明形式:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
* Construct a type with the properties of T except for those in type K.
</span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
type Omit</span>&lt;T, K extends keyof any&gt; = Pick&lt;T, Exclude&lt;keyof T, K&gt;&gt;;</pre>
</div>
<p>使用示例如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">interface IPerson {
name: string;
age: number;
height: number;
}

const person: Omit</span>&lt;IPerson, "age" | "height"&gt; =<span style="color: rgba(0, 0, 0, 1)"> {
name: </span>"zhangsan"<span style="color: rgba(0, 0, 0, 1)">;
}</span></pre>
</div>
<h3>8. ReturnType</h3>
<p>ReturnType会返回函数返回值的类型,其声明形式如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
* Obtain the return type of a function type
</span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
type ReturnType</span>&lt;T extends (...args: any) =&gt; any&gt; = T extends (...args: any) =&gt; infer R ? R : any;</pre>
</div>
<p>使用示例如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">function</span> foo(type): <span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">return</span> type === 0<span style="color: rgba(0, 0, 0, 1)">
}

type FooType </span>= ReturnType&lt;<span style="color: rgba(0, 0, 255, 1)">typeof</span> foo&gt;</pre>
</div>
<p>这里使用 typeof 是为了获取 foo 的函数签名,等价于 (type: any) =&gt; boolean。</p>
<h2>七、Axios 封装</h2>
<p>在React项目中,我们经常使用Axios库进行数据请求,Axios 是基于 Promise 的 HTTP 库,可以在浏览器和 node.js 中使用。Axios 具备以下特性:</p>
<ul>
<li>
<p>从浏览器中创建 XMLHttpRequests;</p>
</li>
<li>
<p>从 node.js 创建 HTTP 请求;</p>
</li>
<li>
<p>支持 Promise API;</p>
</li>
<li>
<p>拦截请求和响应;</p>
</li>
<li>
<p>转换请求数据和响应数据;</p>
</li>
<li>
<p>取消请求;</p>
</li>
<li>
<p>自动转换 JSON 数据;</p>
</li>
<li>
<p>客户端支持防御 XSRF。</p>
</li>
</ul>
<p>Axios的基本使用就不再多介绍了。为了更好地调用,做一些全局的拦截,通常会对Axios进行封装,下面就使用TypeScript对Axios进行简单封装,使其同时能够有很好的类型支持。Axios是自带声明文件的,所以我们无需额外的操作。</p>
<p>下面来看基本的封装:</p>
<div class="cnblogs_code">
<pre>import axios, { AxiosInstance, AxiosRequestConfig, AxiosPromise,AxiosResponse } from 'axios'; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 引入axios和定义在node_modules/axios/index.ts文件里的类型声明</span>

<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 定义接口请求类,用于创建axios请求实例</span>
<span style="color: rgba(0, 0, 0, 1)">class HttpRequest {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 接收接口请求的基本路径</span>
<span style="color: rgba(0, 0, 0, 1)">constructor(public baseUrl: string) {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.baseUrl =<span style="color: rgba(0, 0, 0, 1)"> baseUrl;
}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 调用接口时调用实例的这个方法,返回AxiosPromise</span>
<span style="color: rgba(0, 0, 0, 1)">public request(options: AxiosRequestConfig): AxiosPromise {
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 创建axios实例,它是函数,同时这个函数包含多个属性</span>
    const instance: AxiosInstance =<span style="color: rgba(0, 0, 0, 1)"> axios.create()
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 合并基础路径和每个接口单独传入的配置,比如url、参数等</span>
    options = <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.mergeConfig(options)
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 调用interceptors方法使拦截器生效</span>
    <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.interceptors(instance, options.url)
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 返回AxiosPromise</span>
    <span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> instance(options)
}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 用于添加全局请求和响应拦截</span>
private interceptors(instance: AxiosInstance, url?<span style="color: rgba(0, 0, 0, 1)">: string) {
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 请求和响应拦截</span>
<span style="color: rgba(0, 0, 0, 1)">}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 用于合并基础路径配置和接口单独配置</span>
<span style="color: rgba(0, 0, 0, 1)">private mergeConfig(options: AxiosRequestConfig): AxiosRequestConfig {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> Object.assign({ baseURL: <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.baseUrl }, options);
}
}
export </span><span style="color: rgba(0, 0, 255, 1)">default</span> HttpRequest;</pre>
</div>
<p>通常baseUrl在开发环境的和生产环境的路径是不一样的,所以可以根据当前是开发环境还是生产环境做判断,应用不同的基础路径。这里要写在一个配置文件里:</p>
<div class="cnblogs_code">
<pre>export <span style="color: rgba(0, 0, 255, 1)">default</span><span style="color: rgba(0, 0, 0, 1)"> {
    api: {
      devApiBaseUrl: </span>'/test/api/xxx'<span style="color: rgba(0, 0, 0, 1)">,
      proApiBaseUrl: </span>'/api/xxx'<span style="color: rgba(0, 0, 0, 1)">,
    },
};</span></pre>
</div>
<p>在上面的文件中引入这个配置:</p>
<div class="cnblogs_code">
<pre>import { api: { devApiBaseUrl, proApiBaseUrl } } from '@/config'<span style="color: rgba(0, 0, 0, 1)">;
const apiBaseUrl </span>= env.NODE_ENV === 'production' ? proApiBaseUrl : devApiBaseUrl;</pre>
</div>
<p>之后就可以将apiBaseUrl作为默认值传入HttpRequest的参数:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">class HttpRequest {
constructor(public baseUrl: string </span>=<span style="color: rgba(0, 0, 0, 1)"> apiBaseUrl) {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.baseUrl =<span style="color: rgba(0, 0, 0, 1)"> baseUrl;
}</span></pre>
</div>
<p>接下来可以完善一下拦截器类,在类中interceptors方法内添加请求拦截器和响应拦截器,实现对所有接口请求的统一处理:</p>
<div class="cnblogs_code">
<pre>private interceptors(instance: AxiosInstance, url?<span style="color: rgba(0, 0, 0, 1)">: string) {
   </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 请求拦截</span>
    instance.interceptors.request.use((config: AxiosRequestConfig) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
      </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 接口请求的所有配置,可以在axios.defaults修改配置</span>
      <span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> config
    },
    (error) </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> {
      </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> Promise.reject(error)
    })

   </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 响应拦截</span>
    instance.interceptors.response.use((res: AxiosResponse) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
      const { data } </span>=<span style="color: rgba(0, 0, 0, 1)"> res
      const { code, msg } </span>=<span style="color: rgba(0, 0, 0, 1)"> data
      </span><span style="color: rgba(0, 0, 255, 1)">if</span> (code !== 0<span style="color: rgba(0, 0, 0, 1)">) {
      console.error(msg)
      }
      </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> res
    },
    (error) </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> {
      </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> Promise.reject(error)
    })
}</span></pre>
</div>
<p>到这里封装的就差不多了,一般服务端会将状态码、提示信息和数据封装在一起,然后作为数据返回,所以所有请求返回的数据格式都是一样的,所以就可以定义一个接口来指定返回的数据结构,可以定义一个接口:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">export interface ResponseData {
code: number
data</span>?<span style="color: rgba(0, 0, 0, 1)">: any
msg: string
}</span></pre>
</div>
<p>接下来看看使用TypeScript封装的Axios该如何使用。可以先定义一个请求实例:</p>
<div class="cnblogs_code">
<pre>import HttpRequest from '@/utils/axios'<span style="color: rgba(0, 0, 0, 1)">
export </span>* from '@/utils/axios'<span style="color: rgba(0, 0, 0, 1)">
export </span><span style="color: rgba(0, 0, 255, 1)">default</span> <span style="color: rgba(0, 0, 255, 1)">new</span> HttpRequest()</pre>
</div>
<p>这里把请求类导入进来,默认导出这个类的实例。之后创建一个登陆接口请求方法:</p>
<div class="cnblogs_code">
<pre>import axios, { ResponseData } from './index'<span style="color: rgba(0, 0, 0, 1)">
import { AxiosPromise } from </span>'axios'<span style="color: rgba(0, 0, 0, 1)">

interface ILogin {
user: string;
password: number </span>|<span style="color: rgba(0, 0, 0, 1)"> string
}

export const loginReq </span>= (data: ILogin): AxiosPromise&lt;ResponseData&gt; =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> axios.request({
    url: </span>'/api/user/login'<span style="color: rgba(0, 0, 0, 1)">,
    data,
    method: </span>'POST'<span style="color: rgba(0, 0, 0, 1)">
})
}</span></pre>
</div>
<p>这里封装登录请求方法loginReq,他的参数必须是我们定义的ILogin接口的类型。这个方法返回一个类型为<code>AxiosPromise</code>的Promise,AxiosPromise是axios声明文件内置的类型,可以传入一个泛型变量参数,用于指定返回的结果中data字段的类型。</p>
<p>接下来可以调用一下这个登录的接口:</p>
<div class="cnblogs_code">
<pre>import { loginReq } from '@/api/user'<span style="color: rgba(0, 0, 0, 1)">

const Home: FC </span>= () =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
const login </span>= (params) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
   loginReq(params).then((res) </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> {
   console.log(res.data.code)
   })
}
}</span></pre>
</div>
<p>通过这种方式,当我们调用loginReq接口时,就会提示我们,参数的类型是ILogin,需要传入几个参数。这样编写代码的体验就会好很多。</p>
<h2>八. 其他</h2>
<h3>1. import React</h3>
<p>在React项目中使用TypeScript时,普通组件文件后缀为.tsx,公共方法文件后缀为.ts。在. tsx 文件中导入 React 的方式如下:</p>
<div class="cnblogs_code">
<pre>import * as React from 'react'<span style="color: rgba(0, 0, 0, 1)">
import </span>* as ReactDOM from 'react-dom'</pre>
</div>
<p>这是一种面向未来的导入方式,如果想在项目中使用以下导入方式:</p>
<div class="cnblogs_code">
<pre>import React from "react"<span style="color: rgba(0, 0, 0, 1)">;
import ReactDOM from </span>"react-dom";</pre>
</div>
<p>就需要在tsconfig.json配置文件中进行如下配置:</p>
<div class="cnblogs_code">
<pre>"compilerOptions"<span style="color: rgba(0, 0, 0, 1)">: {
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 允许默认从没有默认导出的模块导入。</span>
    "allowSyntheticDefaultImports": <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">,
}</span></pre>
</div>
<h3>2. Types or Interfaces?</h3>
<p>我们可以使用types或者Interfaces来定义类型吗,那么该如何选择他俩呢?建议如下:</p>
<ul>
<li>
<p>在定义公共 API 时(比如编辑一个库)使用 interface,这样可以方便使用者继承接口,这样允许使用者通过声明合并来扩展它们;</p>
</li>
<li>
<p>在定义组件属性(Props)和状态(State)时,建议使用 type,因为 type 的约束性更强。</p>
</li>
</ul>
<p>interface 和 type 在 ts 中是两个不同的概念,但在 React 大部分使用的 case 中,interface 和 type 可以达到相同的功能效果,type 和 interface 最大的区别是:type 类型不能二次编辑,而 interface 可以随时扩展:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">interface Animal {
name: string
}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 可以继续在原属性基础上,添加新属性:color</span>
<span style="color: rgba(0, 0, 0, 1)">interface Animal {
color: string
}

type Animal </span>=<span style="color: rgba(0, 0, 0, 1)"> {
name: string
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> type类型不支持属性扩展</span><span style="color: rgba(0, 128, 0, 1)">
//</span><span style="color: rgba(0, 128, 0, 1)"> Error: Duplicate identifier 'Animal'</span>
type Animal =<span style="color: rgba(0, 0, 0, 1)"> {
color: string
}</span></pre>
</div>
<p>type对于联合类型是很有用的,比如:type Type = TypeA | TypeB。而interface更适合声明字典类行,然后定义或者扩展它。</p>
<h3>3. 懒加载类型</h3>
<p>如果我们想在React router中使用懒加载,React也为我们提供了懒加载方法的类型,来看下面的例子:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">export interface RouteType {
    pathname: string;
    component: LazyExoticComponent</span>&lt;any&gt;<span style="color: rgba(0, 0, 0, 1)">;
    exact: </span><span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)">;
    title</span>?<span style="color: rgba(0, 0, 0, 1)">: string;
    icon</span>?<span style="color: rgba(0, 0, 0, 1)">: string;
    children</span>?<span style="color: rgba(0, 0, 0, 1)">: RouteType[];
}
export const AppRoutes: RouteType[] </span>=<span style="color: rgba(0, 0, 0, 1)"> [
    {
      pathname: </span>'/login'<span style="color: rgba(0, 0, 0, 1)">,
      component: lazy(() </span>=&gt; import('../views/Login/Login'<span style="color: rgba(0, 0, 0, 1)">)),
      exact: </span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">
    },
    {
      pathname: </span>'/404'<span style="color: rgba(0, 0, 0, 1)">,
      component: lazy(() </span>=&gt; import('../views/404/404'<span style="color: rgba(0, 0, 0, 1)">)),
      exact: </span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">,
    },
    {
      pathname: </span>'/'<span style="color: rgba(0, 0, 0, 1)">,
      exact: </span><span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">,
      component: lazy(() </span>=&gt; import('../views/Admin/Admin'<span style="color: rgba(0, 0, 0, 1)">))
    }
]</span></pre>
</div>
<p>下面是懒加载类型和lazy方法在声明文件中的定义:</p>
<div class="cnblogs_code">
<pre>type LazyExoticComponent&lt;T extends ComponentType&lt;any&gt;&gt; = ExoticComponent&lt;ComponentPropsWithRef&lt;T&gt;&gt; &amp;<span style="color: rgba(0, 0, 0, 1)"> {
readonly _result: T;
};

</span><span style="color: rgba(0, 0, 255, 1)">function</span> lazy&lt;T extends ComponentType&lt;any&gt;&gt;<span style="color: rgba(0, 0, 0, 1)">(
factory: () </span>=&gt; Promise&lt;{ <span style="color: rgba(0, 0, 255, 1)">default</span>: T }&gt;<span style="color: rgba(0, 0, 0, 1)">
): LazyExoticComponent</span>&lt;T&gt;;</pre>
</div>
<h3>4. 类型断言</h3>
<p>类型断言(Type Assertion)可以用来手动指定一个值的类型。在React项目中,断言还是很有用的,。有时候推断出来的类型并不是真正的类型,很多时候我们可能会比TS更懂我们的代码,所以可以使用断言(使用as关键字)来定义一个值得类型。</p>
<p>来看下面的例子:</p>
<div class="cnblogs_code">
<pre>const getLength = (target: string | number): number =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (target.length) { <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> error 类型"string | number"上不存在属性"length"</span>
    <span style="color: rgba(0, 0, 255, 1)">return</span> target.length; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> error类型"number"上不存在属性"length"</span>
} <span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> target.toString().length;
}
};</span></pre>
</div>
<p>当TypeScript不确定一个联合类型的变量到底是哪个类型时,就只能访问此联合类型的所有类型里共有的属性或方法,所以现在加了对参数target和返回值的类型定义之后就会报错。这时就可以使用断言,将target的类型断言成string类型:</p>
<div class="cnblogs_code">
<pre>const getStrLength = (target: string | number): number =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> ((target as string).length) {      
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (target as string).length;
} </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> target.toString().length;
}
};</span></pre>
</div>
<p>需要注意,类型断言并不是类型转换,断言成一个联合类型中不存在的类型是不允许的。</p>
<p>再来看一个例子,在调用一个方法时传入参数:</p>
<p><img src="https://img2023.cnblogs.com/blog/1022151/202302/1022151-20230224171007223-1847969690.png"></p>
<p>这里就提示我们这个参数可能是undefined,而通过业务知道这个值是一定存在的,所以就可以将它断言成数字:<code>data?.subjectId as number</code></p>
<p>除此之外,上面所说的标签类型、组件类型、时间类型都可以使用断言来指定给一些数据,还是要根据实际的业务场景来使用。</p>
<p>感悟:使用类型断言真的能解决项目中的很多报错~</p>
<h3>5. 枚举类型</h3>
<p>枚举类型在项目中的作用也是不可忽视的,使用枚举类型可以让代码的扩展性更好,当我想更改某属性值时,无需去全局更改这个属性,只要更改枚举中的值即可。通常情况下,最好新建一个文件专门来定义枚举值,便于引用。</p>
<div>&nbsp;</div>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div>
</div>
</div><br><br>
来源:https://www.cnblogs.com/momo798/p/17151903.html
頁: [1]
查看完整版本: 如何优雅地在 React 中使用TypeScript,看这一篇就够了!