大家好,我是 CUGGZ。,在单独使用 TypeScript 时没有太多坑,不过和 React 结合之后就会复杂很多。下面就来看一看如何在 React 项目中优雅的使用 TypeScript!,在React中,组件的声明方式有两种:函数组件和类组件, 来看看这两种类型的组件声明时是如何定义TS类型的。,类组件的定义形式有两种:React.Component<P, S={}> 和 React.PureComponent<P, S={} SS={}>,它们都是泛型接口,接收两个参数,第一个是props类型的定义,第二个是state类型的定义,这两个参数都不是必须的,没有时可以省略:,React.PureComponent<P, S={} SS={}> 也是差不多的:,React.PureComponent是有第三个参数的,它表示getSnapshotBeforeUpdate的返回值。,那PureComponent和Component 的区别是什么呢?它们的主要区别是PureComponent中的shouldComponentUpdate 是由自身进行处理的,不需要我们自己处理,所以PureComponent可以在一定程度上提升性能。,有时候可能会见到这种写法,实际上和上面的效果是一样的:,那如果定义时候我们不知道组件的props的类型,只有在调用时才知道组件类型,该怎么办呢?这时泛型就发挥作用了:,通常情况下,函数组件我是这样写的:,除此之外,函数类型还可以使用React.FunctionComponent<P={}>来定义,也可以使用其简写React.FC<P={}>,两者效果是一样的。它是一个泛型接口,可以接收一个参数,参数表示props的类型,这个参数不是必须的。它们就相当于这样:,最终的定义形式如下:,当使用这种形式来定义函数组件时,props中默认会带有children属性,它表示该组件在调用时,其内部的元素,来看一个例子,首先定义一个组件,组件中引入了Child1和Child2组件:,Child1组件结构如下:,我们在Child1组件中打印了children属性,它的值是一个数组,包含Child2对象和后面的文本:,
,使用 React.FC 声明函数组件和普通声明的区别如下:,那如果我们在定义组件时不知道props的类型,只有调用时才知道,那就还是用泛型来定义props的类型。对于使用function定义的函数组件:,如果使用箭头函数定义的函数组件,直接这样调用时错误的:,必须使用extends关键字来定义泛型参数才能被成功解析:,先来看看JSX.Element类型的声明:,可以看到,JSX.Element是ReactElement的子类型,它没有增加属性,两者是等价的。也就是说两种类型的变量可以相互赋值。,JSX.Element 可以通过执行 React.createElement 或是转译 JSX 获得:,React 的类型声明文件中提供了 React.ReactElement<T>,它可以让我们通过传入<T/>来注解类组件的实例化,它在声明文件中的定义如下:,ReactElement是一个接口,包含type,props,key三个属性值。该类型的变量值只能是两种:null 和 ReactElement实例。,通常情况下,函数组件返回ReactElement(JXS.Element)的值。,ReactNode类型的声明如下:,可以看到,ReactNode是一个联合类型,它可以是string、number、ReactElement、null、boolean、ReactNodeArray。由此可知。ReactElement类型的变量可以直接赋值给ReactNode类型的变量,但反过来是不行的。,类组件的 render 成员函数会返回 ReactNode 类型的值:,上面的代码中,给component变量设置了类型是Mycomponent类型的react实例,这时只能给其赋值其为MyComponent的实例组件。,通常情况下,类组件通过 render() 返回 ReactNode的值。,先来看看React的声明文件中对CSSProperties 的定义:,React.CSSProperties是React基于TypeScript定义的CSS属性类型,可以将一个方法的返回值设置为该类型:,这里divStyle组件的返回值就是React.CSSProperties类型。,我们还可以定义一个CSSProperties类型的变量:,这个变量可以在HTML标签的style属性上使用:,在React的类型声明文件中,style属性的类型如下:,默认情况下,React会为根据设置的state的初始值来自动推导state以及更新函数的类型:,
,如果已知state 的类型,可以通过以下形式来自定义state的类型:,如果初始值为null,需要显式地声明 state 的类型:,如果state是一个对象,想要初始化一个空对象,可以使用断言来处理:,实际上,这里将空对象{}断言为IUser接口就是欺骗了TypeScript的编译器,由于后面的代码可能会依赖这个对象,所以应该在使用前及时初始化 user 的值,否则就会报错。,下面是声明文件中 useState 的定义:,可以看到,这里定义两种形式,分别是有初始值和没有初始值的形式。,useEffect的主要作用就是处理副作用,它的第一个参数是一个函数,表示要清除副作用的操作,第二个参数是一组值,当这组值改变时,第一个参数的函数才会执行,这让我们可以控制何时运行函数来处理副作用:,当函数的返回值不是函数或者effect函数中未定义的内容时,如下:,TypeScript就会报错:,
,来看看useEffect在类型声明文件中的定义:,可以看到,useEffect的第一个参数只允许返回一个函数。,当使用 useRef 时,我们可以访问一个可变的引用对象。可以将初始值传递给 useRef,它用于初始化可变 ref 对象公开的当前属性。当我们使用useRef时,需要给其指定类型:,这里给实例的类型指定为了input输入框类型。,当useRef的初始值为null时,有两种创建的形式,第一种:,这种形式下,ref1.current是只读的(read-only),所以当我们将它的innerText属性重新赋值时会报以下错误:,那该怎么将current属性变为动态可变得的,先来看看类型声明文件中 useRef 是如何定义的:,这段代码的第十行的告诉我们,如果需要useRef的直接可变,就需要在泛型参数中包含'| null',所以这就是当初始值为null的第二种定义形式:,这种形式下,nameInput.current就是可写的。不过两种类型在使用时都需要做类型检查:,那么问题来了,为什么第一种写法在没有操作current时没有报错呢?因为useRef在类型定义式具有多个重载声明,第一种方式就是执行的以下函数重载:,从上useRef的声明中可以看到,function useRef的返回值类型化是MutableRefObject,这里面的T就是参数的类型T,所以最终nameInput 的类型就是React.MutableRefObject。,注意,上面用到了HTMLInputElement类型,这是一个标签类型,这个操作就是用来访问DOM元素的。,先来看看类型声明文件中对useCallback的定义:,useCallback接收一个回调函数和一个依赖数组,只有当依赖数组中的值发生变化时才会重新执行回调函数。来看一个例子:,这里我们没有给回调函数中的参数a定义类型,所以下面的调用方式都不会报错:,尽管add方法的两个参数都是number类型,但是上述调用都能够用执行。所以为了更加严谨,我们需要给回调函数定义具体的类型:,这时候如果再给回调函数传入字符串就会报错了:,
,所有,需要注意,在使用useCallback时需要给回调函数的参数指定类型。,先来看看类型声明文件中对useMemo的定义:,useMemo和useCallback是非常类似的,但是它返回的是一个值,而不是函数。所以在定义useMemo时需要定义返回值的类型:,如果返回值不一致,就会报错:,useContext需要提供一个上下文对象,并返回所提供的上下文的值,当提供者更新上下文对象时,引用这些上下文对象的组件就会重新渲染:,在使用useContext时,会自动推断出提供的上下文对象的类型,所以并不需要我们手动设置context的类型。当前,我们也可以使用泛型来设置context的类型:,下面是useContext在类型声明文件中的定义:,有时我们需要处理一些复杂的状态,并且可能取决于之前的状态。这时候就可以使用useReducer,它接收一个函数,这个函数会根据之前的状态来计算一个新的state。其语法如下:,来看下面的例子:,当前的状态是无法推断出来的,可以给reducer函数添加类型,通过给reducer函数定义state和action来推断 useReducer 的类型,下面来修改上面的例子:,这样,在Counter函数中就可以推断出类型。当我们视图使用一个不存在的类型时,就会报错:,除此之外,还可以使用泛型的形式来实现reducer函数的类型定义:,其实dispatch方法也是有类型的:,
,可以看到,dispatch的类型是:React.Dispatch,上面示例的完整代码如下:,在开发中我们会经常在事件处理函数中使用event事件对象,比如在input框输入时实时获取输入的值;使用鼠标事件时,通过 clientX、clientY 获取当前指针的坐标等等。,我们知道,Event是一个对象,并且有很多属性,这时很多人就会把 event 类型定义为any,这样的话TypeScript就失去了它的意义,并不会对event事件进行静态检查,如果一个键盘事件触发了下面的方法,也不会报错:,由于Event事件对象中有很多的属性,所以我们也不方便把所有属性及其类型定义在一个interface中,所以React在声明文件中给我们提供了Event事件对象的类型声明。,常见的Event 事件对象如下:,可以看到,这些Event事件对象的泛型中都会接收一个Element元素的类型,这个类型就是我们绑定这个事件的标签元素的类型,标签元素类型将在下面的第五部分介绍。,来看一个简单的例子:,这里就给onChange方法的事件对象定义为了FormEvent类型,并且作用的对象时一个HTMLInputElement类型的标签(input标签),可以来看下MouseEvent事件对象和ChangeEvent事件对象的类型声明,其他事件对象的声明形似也类似:,在很多事件对象的声明文件中都可以看到 EventTarget 的身影。这是因为,DOM的事件操作(监听和触发),都定义在EventTarget接口上。EventTarget 的类型声明如下:,比如在change事件中,会使用的e.target来获取当前的值,它的的类型就是EventTarget。来看下面的例子:,这里定义了一个input输入框,当触发onChange事件时,会调用onSourceChange方法,该方法的参数e的类型就是:React.ChangeEvent,而e.target的类型就是EventTarget:,
,在来看一个例子:,这点代码中,点击某个盒子,就将它设置为当前的盒子,方便执行其他操作。当鼠标点击盒子时,会触发handleChangeCurren方法,该方法有两个参数,第二个参数是event对象,在方法中执行了e.stopPropagation();是为了阻止冒泡事件,这里的stopPropagation()实际上并不是鼠标事件MouseEvent的属性,它是合成事件上的属性,来看看声明文件中的定义:,可以看到,这里的stopPropagation()是一层层的继承来的,最终来自于BaseSyntheticEvent合成事件类型。原生的事件集合SyntheticEvent就是继承自合成时间类型。SyntheticEvent<T = Element, E = Event>泛型接口接收当前的元素类型和事件类型,如果不介意这两个参数的类型,完全可以这样写:,说完事件对象类型,再来看看事件处理函数的类型。React也为我们提供了贴心的提供了事件处理函数的类型声明,来看看所有的事件处理函数的类型声明:,这里面的T的类型也都是Element,指的是触发该事件的HTML标签元素的类型,下面第五部分会介绍。,EventHandler会接收一个E,它表示事件处理函数中 Event 对象的类型。bivarianceHack 是事件处理函数的类型定义,函数接收一个 Event 对象,并且其类型为接收到的泛型变量 E 的类型, 返回值为 void。,还看上面的那个例子:,这里给onChange方法定义了方法的类型,它是一个ChangeEventHandler的类型,并且作用的对象时一个HTMLImnputElement类型的标签(input标签)。,在项目的依赖文件中可以找到HTML标签相关的类型声明文件:,
,所有的HTML标签的类型都被定义在 intrinsicElements 接口中,常见的标签及其类型如下:,那什么时候会使用到标签类型呢,上面第四部分的Event事件类型和事件处理函数类型中都使用到了标签的类型。上面的很多的类型都需要传入一个ELement类型的泛型参数,这个泛型参数就是对应的标签类型值,可以根据标签来选择对应的标签类型。这些类型都继承自HTMLElement类型,如果使用时对类型类型要求不高,可以直接写HTMLELement。比如下面的例子:,其实,在直接操作DOM时也会用到标签类型,虽然我们现在通常会使用框架来开发,但是有时候也避免不了直接操作DOM。比如我在工作中,项目中的某一部分组件是通过npm来引入的其他组的组件,而在很多时候,我有需要动态的去个性化这个组件的样式,最直接的办法就是通过原生JavaScript获取到DOM元素,来进行样式的修改,这时候就会用到标签类型。,来看下面的例子:,这是我最近写的一段代码(略微删改),在第一页有个add-ele元素的时候就删除它。这里我们将item.firstChild断言成了HTMLDivElement类型,如果不断言,item.firstChild的类型就是ChildNode,而ChildNode类型中是不存在classList属性的,所以就就会报错,当我们把他断言成HTMLDivElement类型时,就不会报错了。很多时候,标签类型可以和断言(as)一起使用。,后面在removeChild时又使用了as断言,为什么呢?item.firstChild不是已经自动识别为ChildNode类型了吗?因为TS会认为,我们可能不能获取到类名为paper的元素,所以item.firstChild的类型就被推断为ChildNode | null,我们有时候比TS更懂我们定义的元素,知道页面一定存在paper 元素,所以可以直接将item.firstChild断言成ChildNode类型。,众所周知,每个HTML标签都有自己的属性,比如Input框就有value、width、placeholder、max-length等属性,下面是Input框的属性类型定义:,如果我们需要直接操作DOM,就可能会用到元素属性类型,常见的元素属性类型如下:,一般情况下,我们是很少需要在项目中显式的去定义标签属性的类型。如果子级去封装组件库的话,这些属性就能发挥它们的作用了。来看例子(来源于网络,仅供学习):,这段代码就是用来封装一个buttom按钮,在button的基础上添加了一些自定义属性,比如上面将button的类型使用交叉类型(&)获得自定义属性和原生 button 属性 :,可以看到,标签属性类型在封装组件库时还是很有用的,更多用途可以自己探索~,在项目中使用一些工具泛型可以提高我们的开发效率,少写很多类型定义。下面来看看有哪些常见的工具泛型,以及其使用方式。,Partial 作用是将传入的属性变为可选项。适用于对类型结构不明确的情况。它使用了两个关键字:keyof和in,先来看看他们都是什么含义。keyof 可以用来取得接口的所有 key 值:,in关键字可以遍历枚举类型,:,keyof 可以产生联合类型, in 可以遍历枚举类型, 所以经常一起使用, 下面是Partial工具泛型的定义:,这里,keyof T 获取 T 所有属性名, 然后使用 in 进行遍历, 将值赋给 P, 最后 T[P] 取得相应属性的值。中间的?就用来将属性设置为可选。,使用示例如下:,Required 的作用是将传入的属性变为必选项,和上面的工具泛型恰好相反,其声明如下:,可以看到,这里使用-?将属性设置为必选,可以理解为减去问号。适用形式和上面的Partial差不多:,将T类型的所有属性设置为只读(readonly),构造出来类型的属性不能被再次赋值。Readonly的声明形式如下:,使用示例如下:,可以看到,通过 Readonly将IPerson的属性转化成了只读,不能再进行赋值操作。,从T类型中挑选部分属性K来构造新的类型。它的声明形式如下:,使用示例如下:,Record 用来构造一个类型,其属性名的类型为K,属性值的类型为T。这个工具泛型可用来将某个类型的属性映射到另一个类型上,下面是其声明形式:,使用示例如下:,Exclude 就是从一个联合类型中排除掉属于另一个联合类型的子集,下面是其声明的形式:,使用示例如下:,上面的Pick 和 Exclude 都是最基础基础的工具泛型,很多时候用 Pick 或者 Exclude 还不如直接写类型更直接。而 Omit 就基于这两个来做的一个更抽象的封装,它允许从一个对象中剔除若干个属性,剩下的就是需要的新类型。下面是它的声明形式:,使用示例如下:,ReturnType会返回函数返回值的类型,其声明形式如下:,使用示例如下:,这里使用 typeof 是为了获取 foo 的函数签名,等价于 (type: any) => boolean。,在React项目中使用TypeScript时,普通组件文件后缀为.tsx,公共方法文件后缀为.ts。在. tsx 文件中导入 React 的方式如下:,这是一种面向未来的导入方式,如果想在项目中使用以下导入方式:,就需要在tsconfig.json配置文件中进行如下配置:,我们可以使用types或者Interfaces来定义类型吗,那么该如何选择他俩呢?建议如下:,在定义公共 API 时(比如编辑一个库)使用 interface,这样可以方便使用者继承接口,这样允许使用最通过声明合并来扩展它们;,在定义组件属性(Props)和状态(State)时,建议使用 type,因为 type 的约束性更强。,interface 和 type 在 ts 中是两个不同的概念,但在 React 大部分使用的 case 中,interface 和 type 可以达到相同的功能效果,type 和 interface 最大的区别是:type 类型不能二次编辑,而 interface 可以随时扩展:,type对于联合类型是很有用的,比如:type Type = TypeA | TypeB。而interface更适合声明字典类行,然后定义或者扩展它。,如果我们想在React router中使用懒加载,React也为我们提供了懒加载方法的类型,来看下面的例子:,下面是懒加载类型和lazy方法在声明文件中的定义:,类型断言(Type Assertion)可以用来手动指定一个值的类型。在React项目中,断言还是很有用的,。有时候推断出来的类型并不是真正的类型,很多时候我们可能会比TS更懂我们的代码,所以可以使用断言(使用as关键字)来定义一个值得类型。,来看下面的例子:,当TypeScript不确定一个联合类型的变量到底是哪个类型时,就只能访问此联合类型的所有类型里共有的属性或方法,所以现在加了对参数target和返回值的类型定义之后就会报错。这时就可以使用断言,将target的类型断言成string类型:,需要注意,类型断言并不是类型转换,断言成一个联合类型中不存在的类型是不允许的。,再来看一个例子,在调用一个方法时传入参数:,
,这里就提示我们这个参数可能是undefined,而通过业务知道这个值是一定存在的,所以就可以将它断言成数字:data?.subjectId as number,除此之外,上面所说的标签类型、组件类型、时间类型都可以使用断言来指定给一些数据,还是要根据实际的业务场景来使用。
© 版权声明
文章版权归作者所有,未经允许请勿转载。