如何解决 TypeScript 对 React props 的类型检查
最近在学 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论