毕业已有3月有余,工作用的技术栈主要是React hooks + TypeScript。其实在单独使用 TypeScript 时没有太多的坑,不过和React结合之后就会复杂很多。本文就来聊一聊TypeScript与React一起使用时经常遇到的一些类型定义的问题。阅读本文前,希望你能有一定的React和TypeScript基础。文章内容较多,建议先收藏再学习!
一、组件声明
在React中,组件的声明方式有两种:函数组件和类组件, 来看看这两种类型的组件声明时是如何定义TS类型的。
1. 类组件
类组件的定义形式有两种:React.Component<P, S={}>
和 React.PureComponent<P, S={} SS={}>
,它们都是泛型接口,接收两个参数,第一个是props类型的定义,第二个是state类型的定义,这两个参数都不是必须的,没有时可以省略:
interface IProps { name: string; } interface IState { count: number; } class App extends React.Component<IProps, IState> { state = { count: 0 }; render() { return ( <div> {this.state.count} {this.props.name} </div> ); } } export default App; 复制代码
React.PureComponent<P, S={} SS={}>
也是差不多的:
class App extends React.PureComponent<IProps, IState> {} 复制代码
React.PureComponent
是有第三个参数的,它表示getSnapshotBeforeUpdate
的返回值。
那PureComponent和Component 的区别是什么呢?它们的主要区别是PureComponent中的shouldComponentUpdate 是由自身进行处理的,不需要我们自己处理,所以PureComponent可以在一定程度上提升性能。
有时候可能会见到这种写法,实际上和上面的效果是一样的:
import React, {PureComponent, Component} from "react"; class App extends PureComponent<IProps, IState> {} class App extends Component<IProps, IState> {} 复制代码
那如果定义时候我们不知道组件的props的类型,只有在调用时才知道组件类型,该怎么办呢?这时泛型就发挥作用了:
// 定义组件 class MyComponent<P> extends React.Component<P> { internalProp: P; constructor(props: P) { super(props); this.internalProp = props; } render() { return ( <span>hello world</span> ); } } // 使用组件 type IProps = { name: string; age: number; }; <MyComponent<IProps> name="React" age={18} />; // Success <MyComponent<IProps> name="TypeScript" age="hello" />; // Error 复制代码
2. 函数组件
通常情况下,函数组件我是这样写的:
interface IProps { name: string } const App = (props: IProps) => { const {name} = props; return ( <div className="App"> <h1>hello world</h1> <h2>{name}</h2> </div> ); } export default App; 复制代码
除此之外,函数类型还可以使用React.FunctionComponent<P={}>
来定义,也可以使用其简写React.FC<P={}>
,两者效果是一样的。它是一个泛型接口,可以接收一个参数,参数表示props的类型,这个参数不是必须的。它们就相当于这样:
type React.FC<P = {}> = React.FunctionComponent<P> 复制代码
最终的定义形式如下:
interface IProps { name: string } const App: React.FC<IProps> = (props) => { const {name} = props; return ( <div className="App"> <h1>hello world</h1> <h2>{name}</h2> </div> ); } export default App; 复制代码
当使用这种形式来定义函数组件时,props中默认会带有children属性,它表示该组件在调用时,其内部的元素,来看一个例子,首先定义一个组件,组件中引入了Child1和Child2组件:
import Child1 from "./child1"; import Child2 from "./child2"; interface IProps { name: string; } const App: React.FC<IProps> = (props) => { const { name } = props; return ( <Child1 name={name}> <Child2 name={name} /> TypeScript </Child1> ); }; export default App; 复制代码
Child1组件结构如下:
interface IProps { name: string; } const Child1: React.FC<IProps> = (props) => { const { name, children } = props; console.log(children); return ( <div className="App"> <h1>hello child1</h1> <h2>{name}</h2> </div> ); }; export default Child1; 复制代码
我们在Child1组件中打印了children属性,它的值是一个数组,包含Child2对象和后面的文本:
使用 React.FC 声明函数组件和普通声明的区别如下:
- React.FC 显式地定义了返回类型,其他方式是隐式推导的;
- React.FC 对静态属性:displayName、propTypes、defaultProps 提供了类型检查和自动补全;
- React.FC 为 children 提供了隐式的类型(ReactElement | null)。
那如果我们在定义组件时不知道props的类型,只有调用时才知道,那就还是用泛型来定义props的类型。对于使用function定义的函数组件:
// 定义组件 function MyComponent<P>(props: P) { return ( <span> {props} </span> ); } // 使用组件 type IProps = { name: string; age: number; }; <MyComponent<IProps> name="React" age={18} />; // Success <MyComponent<IProps> name="TypeScript" age="hello" />; // Error 复制代码
如果使用箭头函数定义的函数组件,直接这样调用时错误的:
const MyComponent = <P>(props: P) { return ( <span> {props} </span> ); } 复制代码
必须使用extends关键字来定义泛型参数才能被成功解析:
const MyComponent = <P extends any>(props: P) { return ( <span> {props} </span> ); } 复制代码
二、React内置类型
1. JSX.Element
先来看看JSX.Element类型的声明:
declare global { namespace JSX { interface Element extends React.ReactElement<any, any> { } } } 复制代码
可以看到,JSX.Element是ReactElement的子类型,它没有增加属性,两者是等价的。也就是说两种类型的变量可以相互赋值。
JSX.Element 可以通过执行 React.createElement 或是转译 JSX 获得:
const jsx = <div>hello</div> const ele = React.createElement("div", null, "hello"); 复制代码
2. React.ReactElement
React 的类型声明文件中提供了 React.ReactElement<T>,它可以让我们通过传入<T/>来注解类组件的实例化,它在声明文件中的定义如下:
interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> { type: T; props: P; key: Key | null; } 复制代码
ReactElement是一个接口,包含type,props,key三个属性值。该类型的变量值只能是两种: null 和 ReactElement实例。
通常情况下,函数组件返回ReactElement(JXS.Element)的值。
3. React.ReactNode
ReactNode类型的声明如下:
type ReactText = string | number; type ReactChild = ReactElement | ReactText; interface ReactNodeArray extends Array<ReactNode> {} type ReactFragment = {} | ReactNodeArray; type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined; 复制代码
可以看到,ReactNode是一个联合类型,它可以是string、number、ReactElement、null、boolean、ReactNodeArray。由此可知。ReactElement类型的变量可以直接赋值给ReactNode类型的变量,但反过来是不行的。
类组件的 render 成员函数会返回 ReactNode 类型的值:
class MyComponent extends React.Component { render() { return <div>hello world</div> } } // 正确 const component: React.ReactNode<MyComponent> = <MyComponent />; // 错误 const component: React.ReactNode<MyComponent> = <OtherComponent />; 复制代码
上面的代码中,给component变量设置了类型是Mycomponent类型的react实例,这时只能给其赋值其为MyComponent的实例组件。
通常情况下,类组件通过 render() 返回 ReactNode的值。
4. CSSProperties
先来看看React的声明文件中对CSSProperties 的定义:
export interface CSSProperties extends CSS.Properties<string | number> { /** * The index signature was removed to enable closed typing for style * using CSSType. You're able to use type assertion or module augmentation * to add properties or an index signature of your own. * * For examples and more information, visit: * https://github.com/frenic/csstype#what-should-i-do-when-i-get-type-errors */ } 复制代码
React.CSSProperties是React基于TypeScript定义的CSS属性类型,可以将一个方法的返回值设置为该类型:
import * as React from "react"; const classNames = require("./sidebar.css"); interface Props { isVisible: boolean; } const divStyle = (props: Props): React.CSSProperties => ({ width: props.isVisible ? "23rem" : "0rem" }); export const SidebarComponent: React.StatelessComponent<Props> = props => ( <div id="mySidenav" className={classNames.sidenav} style={divStyle(props)}> {props.children} </div> ); 复制代码
这里divStyle组件的返回值就是React.CSSProperties类型。
我们还可以定义一个CSSProperties类型的变量:
const divStyle: React.CSSProperties = { width: "11rem", height: "7rem", backgroundColor: `rgb(${props.color.red},${props.color.green}, ${props.color.blue})` }; 复制代码
这个变量可以在HTML标签的style属性上使用:
<div style={divStyle} /> 复制代码
在React的类型声明文件中,style属性的类型如下:
style?: CSSProperties | undefined;