优雅的在 react 中使用 TypeScript

栏目: IOS · Android · 发布时间: 6年前

内容简介:讨论几个问题,react 组件的声明?react 高阶组件的声明和使用?class组件中 props 和 state 的使用?...需要特别强调的是,如果用到了这是因为我们使用
  • 为了在 react 中更好的使用 ts,进行一下讨论
  • 怎么合理的再 react 中使用 ts 的一些特性让代码更加健壮

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

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

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

声明React组件

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

  • 类组件的声明

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

需要特别强调的是,如果用到了 state ,除了在声明组件时通过泛型参数传递其 state 结构,还需要在初始化 state 时声明为 readonly

这是因为我们使用 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中的高阶组件本质上是个高阶函数的调用,所以高阶组件的使用,我们既可以使用函数式方法调用,也可以使用装饰器。但是在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 这个函数自身对齐做了正确的类型声明。

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

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

  • 就是将高阶组件注入的属性都声明可选(通过 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的类型里添加这个值:

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必传的问题。这确实是个解决问题的办法。但是就像上一个问题里提到的,这种应对办法应该是对付哪些没有类型声明或者声明不正确的高阶组件的。

所以这个就要求我们能正确的声明高阶组件:

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都做出类型声明。


以上所述就是小编给大家介绍的《优雅的在 react 中使用 TypeScript》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

设计原本

设计原本

Frederick P. Brooks, Jr. / InfoQ中文站、王海鹏、高博 / 机械工业出版社 / 2011-1-1 / 55.00元

无论是软件开发、工程还是建筑,有效的设计都是工作的核心。《设计原本:计算机科学巨匠Frederick P. Brooks的思考》将对设计过程进行深入分析,揭示进行有效和优雅设计的方法。 本书包含了多个行业设计者的特别领悟。Frederick P. Brooks, Jr.精确发现了所有设计项目中内在的不变因素,揭示 了进行优秀设计的过程和模式。通过与几十位优秀设计者的对话,以及他自己在几个设计......一起来看看 《设计原本》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具