优雅的在 react 中使用 TypeScript

写在最前面

  • 为了在 react 中更好的使用 ts,进行一下讨论
  • 怎么合理的再 react 中使用 ts 的一些特性让代码更加健壮

讨论几个问题,react 组件的声明?react 高阶组件的声明和使用?class组件中 props 和 state 的使用?...javascript

在 react 中使用 ts 的几点原则和变化

  • 全部用到jsx语法的文件都须要以tsx后缀命名
  • 使用组件声明时的Component<P, S>泛型参数声明,来代替PropTypes!
  • 全局变量或者自定义的window对象属性,统一在项目根下的global.d.ts中进行声明定义
  • 对于项目中经常使用到的接口数据对象,在types/目录下定义好其结构化类型声明

声明React组件

  • react中的组件从定义方式上来讲,分为类组件和函数式组件。html

  • 类组件的声明java

class App extends Component<IProps, IState> {
    static defaultProps = {
        // ...
    }
    
    readonly state = {
        // ...
    }; 
    // 小技巧:若是state很复杂不想一个个都初始化,能够结合类型断言初始化state为空对象或者只包含少数必须的值的对象: readonly state = {} as IState;
}
复制代码

须要特别强调的是,若是用到了state,除了在声明组件时经过泛型参数传递其state结构,还须要在初始化state时声明为 readonlyreact

这是由于咱们使用 class properties 语法对state作初始化时,会覆盖掉Component<P, S>中对statereadonly标识。数组

函数式组件的声明

// SFC: stateless function components
// v16.7起,因为hooks的加入,函数式组件也可使用state,因此这个命名不许确。新的react声明文件里,也定义了React.FC类型^_^
const List: React.SFC<IProps> = props => null
复制代码

class组件都要指明props和state类型吗?

  • 是的。只要在组件内部使用了propsstate,就须要在声明组件时指明其类型。
  • 可是,你可能发现了,只要咱们初始化了state,貌似即便没有声明state的类型,也能够正常调用以及setState。没错,实际状况确实是这样的,可是这样子作实际上是让组件丢失了对state的访问和类型检查!
// bad one
class App extends Component {
    state = {
        a: 1,
        b: 2
    }
 
    componentDidMount() {
        this.state.a // ok: 1
 
        // 假如经过setState设置并不存在的c,TS没法检查到。
        this.setState({
            c: 3
        });
        
        this.setState(true); // ???
    }
    // ...
}
 
// React Component
class Component<P, S> {
        constructor(props: Readonly<P>);
        setState<K extends keyof S>(
            state: ((prevState: Readonly<S>, props: Readonly<P>) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null),
            callback?: () => void
        ): void;
        forceUpdate(callBack?: () => void): void;
        render(): ReactNode;
        readonly props: Readonly<{ children?: ReactNode }> & Readonly<P>;
        state: Readonly<S>;
        context: any;
        refs: {
            [key: string]: ReactInstance
        };
    }
 
 
// interface IState{
// a: number,
// b: number
// }

// good one
class App extends Component<{}, { a: number, b: number }> {
   
    readonly state = {
        a: 1,
        b: 2
    }
    
    //readonly state = {} as IState,断言所有为一个值
 
    componentDidMount() {
        this.state.a // ok: 1
 
        //正确的使用了 ts 泛型指示了 state 之后就会有正确的提示
        // error: '{ c: number }' is not assignable to parameter of type '{ a: number, b: number }'
        this.setState({
            c: 3
        });
    }
    // ...
}
复制代码

使用react高阶组件

什么是 react 高阶组件?装饰器?react-router

  • 由于react中的高阶组件本质上是个高阶函数的调用,因此高阶组件的使用,咱们既可使用函数式方法调用,也可使用装饰器。可是在TS中,编译器会对装饰器做用的值作签名一致性检查,而咱们在高阶组件中通常都会返回新的组件,而且对被做用的组件的props进行修改(添加、删除)等。这些会致使签名一致性校验失败,TS会给出错误提示。这带来两个问题:

第一,是否还能使用装饰器语法调用高阶组件?

  • 这个答案也得分状况:若是这个高阶组件正确声明了其函数签名,那么应该使用函数式调用,好比 withRouter
import { RouteComponentProps } from 'react-router-dom';
 
const App = withRouter(class extends Component<RouteComponentProps> {
    // ...
});
 
// 如下调用是ok的
<App />
复制代码

如上的例子,咱们在声明组件时,注解了组件的props是路由的RouteComponentProps结构类型,可是咱们在调用App组件时,并不须要给其传递RouteComponentProps里说具备的locationhistory等值,这是由于withRouter这个函数自身对齐作了正确的类型声明。app

第二,使用装饰器语法或者没有函数类型签名的高阶组件怎么办?


如何正确的声明高阶组件?

  • 就是将高阶组件注入的属性都声明可选(经过Partial这个映射类型),或者将其声明到额外的injected组件实例属性上。 咱们先看一个常见的组件声明:
import { RouteComponentProps } from 'react-router-dom';
 
// 方法一
@withRouter
class App extends Component<Partial<RouteComponentProps>> {
    public componentDidMount() {
        // 这里就须要使用非空类型断言了
        this.props.history!.push('/');
    }
    // ...
});
 
// 方法二
@withRouter
class App extends Component<{}> {
    get injected() {
        return this.props as RouteComponentProps
    }
 
    public componentDidMount() {
        this.injected.history.push('/');
    }
    // ...
复制代码

如何正确的声明高阶组件?

interface IUserCardProps {
    name: string;
    avatar: string;
    bio: string;
 
    isAdmin?: boolean;
}
class UserCard extends Component<IUserCardProps> { /* ... */}
复制代码

上面的组件要求了三个必传属性参数:name、avatar、bio,isAdmin是可选的。加入此时咱们想要声明一个高阶组件,用来给UserCard传递一个额外的布尔值属性visible,咱们也须要在UserCard中使用这个值,那么咱们就须要在其props的类型里添加这个值:less

interface IUserCardProps {
    name: string;
    avatar: string;
    bio: string;
    visible: boolean;
 
    isAdmin?: boolean;
}
@withVisible
class UserCard extends Component<IUserCardProps> {
    render() {
        // 由于咱们用到visible了,因此必须在IUserCardProps里声明出该属性
        return <div className={this.props.visible ? '' : 'none'}>...</div>
    }
}
 
function withVisiable(WrappedComponent) {
    return class extends Component {
        render() {
            return <WrappedComponent {..this.props} visiable={true} /> } } } 复制代码
  • 可是这样一来,咱们在调用UserCard时就会出现问题,由于visible这个属性被标记为了必需,因此TS会给出错误。这个属性是由高阶组件注入的,因此咱们确定是不能要求都再传一下的。

可能你此时想到了,把visible声明为可选。没错,这个确实就解决了调用组件时visible必传的问题。这确实是个解决问题的办法。可是就像上一个问题里提到的,这种应对办法应该是对付哪些没有类型声明或者声明不正确的高阶组件的。dom

因此这个就要求咱们能正确的声明高阶组件:函数

interface IVisible {
    visible: boolean;
}
 
 //排除 IVisible
function withVisible<Self>(WrappedComponent: React.ComponentType<Self & IVisible>): React.ComponentType<Omit<Self, 'visible'>> {
    return class extends Component<Self> {
        render() {
            return <WrappedComponent {...this.props} visible={true} /> } } } 复制代码

如上,咱们声明withVisible这个高阶组件时,利用泛型和类型推导,咱们对高阶组件返回的新的组件以及接收的参数组件的props都作出类型声明。

参考:

  • 组内大佬的wiki
相关文章
相关标签/搜索