写在最前面
讨论几个问题,react 组件的声明?react 高阶组件的声明和使用?class组件中 props 和 state 的使用?...
jsx语法的文件都需要以tsx后缀命名Component泛型参数声明,来代替PropTypes!global.d.ts中进行声明定义types/目录下定义好其结构化类型声明class App extends Component<IProps, IState> {
static defaultProps = {
// ...
}
readonly state = {
// ...
};
// 小技巧:如果state很复杂不想一个个都初始化,可以结合类型断言初始化state为空对象或者只包含少数必须的值的对象: readonly state = {} as IState;
}
复制代码需要特别强调的是,如果用到了
state,除了在声明组件时通过泛型参数传递其state结构,还需要在初始化state时声明为readonly
这是因为我们使用 class properties 语法对state做初始化时,会覆盖掉Component中对state的readonly标识。
// SFC: stateless function components
// v16.7起,由于hooks的加入,函数式组件也可以使用state,所以这个命名不准确。新的react声明文件里,也定义了React.FC类型^_^
const List: React.SFC = props => null
复制代码是的。只要在组件内部使用了props和state,就需要在声明组件时指明其类型。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);
setState(
state: ((prevState: Readonly, props: Readonly) => (Pick | S | null)) | (Pick | S | null),
callback?: () => void
): void;
forceUpdate(callBack?: () => void): void;
render(): ReactNode;
readonly props: Readonly<{ children?: ReactNode }> & Readonly;
state: Readonly;
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
});
}
// ...
}
复制代码props进行修改(添加、删除)等。这些会导致签名一致性校验失败,TS会给出错误提示。这带来两个问题:withRouter:import { RouteComponentProps } from 'react-router-dom';
const App = withRouter(class extends Component<RouteComponentProps> {
// ...
});
// 以下调用是ok的
复制代码如上的例子,我们在声明组件时,注解了组件的props是路由的
RouteComponentProps结构类型,但是我们在调用App组件时,并不需要给其传递RouteComponentProps里说具有的location、history等值,这是因为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} />
}
}
}
复制代码可能你此时想到了,把visible声明为可选。没错,这个确实就解决了调用组件时visible必传的问题。这确实是个解决问题的办法。但是就像上一个问题里提到的,这种应对办法应该是对付哪些没有类型声明或者声明不正确的高阶组件的。
所以这个就要求我们能正确的声明高阶组件:
interface IVisible {
visible: boolean;
}
//排除 IVisible
function withVisible<Self>(WrappedComponent: React.ComponentType): React.ComponentType<Omit<Self, 'visible'>> {
return class extends Component<Self> {
render() {
return <WrappedComponent {...this.props} visible={true} />
}
}
}
复制代码如上,我们声明withVisible这个高阶组件时,利用泛型和类型推导,我们对高阶组件返回的新的组件以及接收的参数组件的props都做出类型声明。