实用的TypeScript。 React + Redux

我不了解您的生活状况,如果不严格输入。你在做什么整天借记?

当前,任何现代前端应用程序的开发都比团队正在其上工作的hello world级(其组成定期更改)要复杂得多,这对代码库的质量提出了很高的要求。 为了将代码的质量级别保持在适当的水平, #gostgroup前端团队中的我们始终保持最新状态,并且不怕使用在各种规模公司项目中都能显示出实际益处的现代技术。


静态类型及其在TypeScript实例中的好处已在各篇文章中进行了很多讨论,因此,今天我们将重点关注前端开发人员所面临的更多应用任务,以作为团队最喜欢的堆栈(React + Redux)的一个示例。


“我不理解你的生活状况,如果没有严格的打字。你在做什么。连续数天借记?” -我不认识作者。


“不,我们整天都写类型”-我的同事。


在用TypeScript编写代码时(下文中将隐含主题堆栈),许多人抱怨说他们不得不花费大量时间手动编写类型。 一个很好的例子来说明这个问题是react-reduxconnect connector函数。 让我们看下面的代码:


 type Props = { a: number, b: string; action1: (a: number) => void; action2: (b: string) => void; } class Component extends React.PureComponent<Props> { } connect( (state: RootStore) => ({ a: state.a, b: state.b, }), { action1, action2, }, )(Component); 

这是什么问题? 问题在于,对于通过连接器注入的每个新属性,我们必须在组件属性的常规类型(React)中描述此属性的类型。 您说,这不是一个非常有趣的职业,您仍然希望能够将连接器中的属性类型收集为一种类型,然后将其“连接”一次到组件的常规属性类型。 我有个好消息要给你。 TypeScript让您今天就可以这样做! 准备好了吗 走吧


TypeScript的力量


TypeScript不会停滞不前,并且一直在发展(我喜欢它)。 从2.8版开始,其中出现了一个非常有趣的函数(条件类型),它允许基于条件表达式的类型映射。 我不会在这里详细介绍,而只是留下文档链接并从中插入一段代码作为说明:


 type TypeName<T> = T extends string ? "string" : T extends number ? "number" : T extends boolean ? "boolean" : T extends undefined ? "undefined" : T extends Function ? "function" : "object"; type T0 = TypeName<string>; // "string" type T1 = TypeName<"a">; // "string" type T2 = TypeName<true>; // "boolean" type T3 = TypeName<() => void>; // "function" type T4 = TypeName<string[]>; // "object" 

此功能对我们的情况有何帮助。 react-redux查看react-redux类型描述 ,可以找到InferableComponentEnhancerWithProps类型,该类型用于确保注入的属性的类型不属于在实例化组件时必须显式设置的组件属性的外部类型。 InferableComponentEnhancerWithProps类型具有两个常规参数: TInjectedPropsTNeedsProps 。 我们对第一个感兴趣。 让我们尝试从该连接器中“拉出”这种类型!


 type TypeOfConnect<T> = T extends InferableComponentEnhancerWithProps<infer Props, infer _> ? Props : never ; 

并直接从存储库中提取真实示例中的类型(您可以在其中克隆并运行测试程序):


 import React from 'react'; import { connect } from 'react-redux'; import { RootStore, init, TypeOfConnect, thunkAction, unboxThunk } from 'src/redux'; const storeEnhancer = connect( (state: RootStore) => ({ ...state, }), { init, thunkAction: unboxThunk(thunkAction), } ); type AppProps = {} & TypeOfConnect<typeof storeEnhancer> ; class App extends React.PureComponent<AppProps> { componentDidMount() { this.props.init(); this.props.thunkAction(3000); } render() { return ( <> <div>{this.props.a}</div> <div>{this.props.b}</div> <div>{String(this.props.c)}</div> </> ); } } export default storeEnhancer(App); 

在上面的示例中,我们分为两个阶段将存储库(Redux)连接。 在第一阶段,我们获得了一个更高阶的组件storeEnhancer (又名InferableComponentEnhancerWithProps ),使用TypeOfConnect帮助程序类型从中提取InferableComponentEnhancerWithProps属性类型,并将获得的属性类型与组件的本机属性类型进一步组合(通过&类型的交集)。 在第二阶段,我们仅装饰原始组件。 现在,无论您向连接器添加什么内容,它都将自动属于组件属性类型。 太好了,我们想要实现的目标!


细心的读者会注意到,带有副作用(thunk动作创建者)的动作生成器(为简便起见,我们简化了动作术语)使用unboxThunk函数进行了附加处理。 是什么导致了这种额外措施? 让我们做对。 首先,让我们使用存储库中的程序示例查看此类操作的签名:


 const thunkAction = (delay: number): ThunkAction<void, RootStore, void, AnyAction> => (dispatch) => { console.log('waiting for', delay); setTimeout(() => { console.log('reset'); dispatch(reset()); }, delay); }; 

从签名中可以看到,我们的操作不会立即返回目标函数,而是首先返回一个中间函数, redux-middleware拾取该中间函数以在我们的主要函数中启用副作用。 但是,当在组件属性中以连接形式使用此功能时,该功能的签名会减少,中间功能除外。 如何用类型来描述? 需要特殊的转换器功能。 TypeScript再次显示了其功能。 首先,我们描述从签名中删除中间函数的类型:


 CutMiddleFunction<T> = T extends (...arg: infer Args) => (...args: any[]) => infer R ? (...arg: Args) => R : never ; 

在这里,除了条件类型外,我们还使用TypeScript 3.0的最新创新,它允许我们推论任意数量(剩余参数)数量的函数参数的类型。 有关详细信息,请参见文档 。 现在仍然需要以相当严格的方式削减我们行动中的多余部分:


 const unboxThunk = <Args extends any[], R, S, E, A extends Action<any>>( thunkFn: (...args: Args) => ThunkAction<R, S, E, A>, ) => ( thunkFn as any as CutMiddleFunction<typeof thunkFn> ); 

通过这样的转换器传递动作,我们在输出中获得所需的签名。 现在,该动作可以在连接器中使用了。


因此,通过简单的操作,当在堆栈上编写类型化的代码时,我们减少了手工工作。 如果再进一步,我们也可以像在redux-modus中那样简化action和reducers的类型。


PS当通过功能和redux.bindActionCreators在连接器中使用动作的动态绑定时redux.bindActionCreators我们将需要更redux.bindActionCreators地键入此实用程序(可能是通过编写我们自己的包装器)。


更新0
如果有人认为此解决方案很方便,那么您可以喜欢它,以便将type实用程序添加到@types/react-redux


更新1
您无需使用其他几种类型来显式指定飞节的注入道具的类型。 只需拿起hoki,然后将其中的类型拉出即可:


 export type BasicHoc<T> = (Component: React.ComponentType<T>) => React.ComponentType<any>; export type ConfiguredHoc<T> = (...args: any[]) => (Component: React.ComponentType<T>) => React.ComponentType<any>; export type BasicHocProps<T> = T extends BasicHoc<infer Props> ? Props : never; export type ConfiguredHocProps<T> = T extends ConfiguredHoc<infer Props> ? Props : never; export type HocProps<T> = T extends BasicHoc<any> ? BasicHocProps<T> : T extends ConfiguredHoc<any> ? ConfiguredHocProps<T> : never ; const basicHoc = (Component: React.ComponentType<{a: number}>) => class extends React.Component {}; const configuredHoc = (opts: any) => (Component: React.ComponentType<{a: number}>) => class extends React.Component {}; type props1 = HocProps<typeof basicHoc>; // {a: number} type props2 = HocProps<typeof configuredHoc>; // {a: number} 

更新2
主题的类型现在位于@types/react-reduxConnectedProps )中。

Source: https://habr.com/ru/post/zh-CN431452/


All Articles