如何解决 TypeScript 对 React props 的类型检查

发布于 2022-10-11 22:01:30 字数 5495 浏览 93 评论 0

最近在学 mobx,感觉挺灵活高效的,没有 Redux 那么繁琐的约定,对于一些小项目而言无疑是比较好的选择。同时我的 react 项目都是基于 TypeScript 开发的,所以尝试用Typescrip 来写 Mobx。总体而言,体验还是不错的,但是在使用 Provider/inject 时,遇到了一些问题:

import * as React from 'react';
import { inject, observer, Provider } from 'mobx-react';
import { observable } from 'mobx';
import { ChangeEvent } from 'react';


interface ContextType {
  color: string;
}

@inject('color')
class Message extends React.Component<ContextType> {
  render() {
    return (
      <div>{this.props.color}</div>
    );
  }
}

class MessageWrap extends React.Component {
  render() {
    return (
      <div>
        <Message/>
      </div>
    );
  }
}

@observer
export default class Seven extends React.Component {
  
  @observable
  color: string = 'red';

  changeColor = (event: ChangeEvent<HTMLInputElement>) => {
    this.color = String(event.target.value) as string;
  }

  render() {
    return (
      <Provider color={this.color}>
        <div>
          <MessageWrap/>
          <input onChange={this.changeColor}/>
        </div>
      </Provider>
    );
  }

}

代码就是上面这段,这里遇到的问题是:Provider 基于 Context API;但在其嵌套的子组件(Message)使用 inject 装饰器之后,需要访问 props 来获取从Provider传递下来的observable值,而这个时候Typescript会对React的 props 进行严格的类型检查。所以只能通过层层传递 props 来通过 Typescript 的类型检查,这个时候Context的跨组件传递特性也就没了。这个时候想了一想,不得已只能使用可选属性来规避这个问题了,就像这样:

interface ContextType {
  color?: string;
}

@inject('color')
class Message extends React.Component<ContextType> {
  render() {
    return (
      <div>{this.props.color}</div>
    );
  }
}

通过可选属性,规避掉了Typescript的对Props的类型检查,但这个时候就有潜在的问题了,如果这个时候Provider没有传递color这个observable,也是能通过检查的,所以需要对进行传递过来的observable值进行额外的判断了。在Google上搜了搜相关的内容,发现大家对这个问题都挺困扰的。然后发现了这篇文章: https://medium.com/@prashaantt/strongly-typing-injected-react-props-635a6828acaf

在Typescript2.0之前,空类型是能赋值给其他类型的,就像这样:

let s: string;
s = "some string";
s = null;
s = undefined;

而在Typescript2.0开启了strictNullChecks 严格的空检查之后,就会规避掉上面这些问题,就像下面这样:

let s1: string;
s1 = "some string";
s1 = undefined; // Type 'undefined' is not assignable to type 'string'
s1 = null; // Type 'null' is not assignable to type 'string'

let s2: string | undefined;
s2 = "another string";
s2 = undefined;
s2 = null;  // Type 'null' is not assignable to type 'string | undefined'

但是开启了这个选项后,就会对interface中的可选属性产生影响。还是上面的那个问题,前面也说了需要进行额外的判断,如果不进行判断,就像下面这样:

interface ContextType {
  color?: string;
}

@inject('color')
class Message extends React.Component<ContextType> {
  render() {
    return (
      <div>{this.props.color.toUpperCase()}</div>
    );
  }
}

这个时候编译器会抛出错误:

同时,不止如此,在真实的应用场景中,可能会有更多的可选属性,而且这些属性有可能来自第三方类库的高阶组件,类似于react-router和mobx,这个时候就跟我遇到的问题非常的像了:

import { UserStore } from "../../stores/UserStore";

interface MyComponentProps {
  name: string;
  countryCode?: string;
  userStore?: UserStore;
  router?: InjectedRouter;
}

@inject("userStore")
@withRouter
@observer
class MyComponent extends React.Component<MyComponentProps, {}> {
  render() {
  const { name, countryCode, userStore, router } = this.props;
  
  return (
    <div>
    User: { name } <br />
    Name: { userStore && userStore.getUserName(name) } <br />
    Country: { countryCode && countryCode.toUpperCase() } <br />
    <button
      onClick={() => { router && router.push(`/users/${name}`) }}
      >
      Check out user
    </button>
    </div>
  );
  }
}

在调用上面的组件时,虽然高阶组件会自动将对应的属性注入到组件树中,这也就保证了这些Props是一定存在的,但是Typescript仍然强制开发者去做更多的判断,非常疲惫。

如果你不想去做更多的判断,就不能使用可选属性,像下面这样去定义props的interface:

interface MyComponentProps {
  name: string;
  countryCode?: string;
  userStore: UserStore; // made required
  router: InjectedRouter; // made required
}

但在嵌套使用该组件的时候,你就会非常手足无措了:

class OtherComponent extends React.Component<{}, {}> {
  render() {
  return (
    <MyComponent
    name="foo"
    countryCode="in"
    // Error: 'router' and 'userStore' are missing!
    />
  );
  }
}

有没有更优雅的解决方案呢?当然是有的。我们可以利用接口的继承,具体的解决方案如下:

interface MyComponentProps {
  name: string;
  countryCode?: string;
}

interface InjectedProps extends MyComponentProps {
  userStore: UserStore;
  router: InjectedRouter;
}

@inject("userStore")
@withRouter
@observer
class MyComponent extends React.Component<MyComponentProps, {}> {
  get injected() {
  return this.props as InjectedProps;
  }

  render() {
  const { name, countryCode } = this.props;
  const { userStore, router } = this.injected;
  ...
  }
}

我们将可选属性抽离出来,单独定义成一个接口,然后该接口继承非可选属性的接口。在定义组件的时候只需要传入非可选属性的接口,然后在调用 props 时,利用断言将该非可选属性的接口强制成可选属性的接口,这样就规避掉了 Typescript 对 props 的额外判断,非常优雅。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

0 文章
0 评论
84961 人气
更多

推荐作者

醉城メ夜风

文章 0 评论 0

远昼

文章 0 评论 0

平生欢

文章 0 评论 0

微凉

文章 0 评论 0

Honwey

文章 0 评论 0

qq_ikhFfg

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文