Currently, the development of any modern front-end application is more complex than the hello world
level, which the team is working on (the composition of which changes periodically) places high demands on the quality of the code base. In order to maintain the quality level of the code at the proper level, we in the #gostgroup frontend team are keeping abreast of the times and are not afraid to apply modern technologies that show their practical use in projects of companies of all sizes .
Static typing and its usefulness on the example of TypeScript have been much said in various articles and therefore today we will focus on the more applied tasks that front-end developers face on the example of our favorite stack (React + Redux).
When writing code on TypeScript (hereinafter in the text, the subject stack will be implied), many complain that they have to spend a lot of time writing types manually. A good example illustrating the problem is the connect
function connect
from the react-redux
. Let's take a look at the code below:
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);
What is the problem here? The problem is that for each new property injected through the connector, we have to describe the type of this property in the general type of component properties (React). Not a very interesting activity, tell me, you still want to be able to collect the type of properties from the connector into one type, which you then “connect” once to the general type of component properties. I have good news for you. Already today TypeScript allows you to do this! Ready? Go!
TypeScript does not stand still and is constantly evolving (for which I love it). Starting from version 2.8, a very interesting function (conditional types) has appeared in it, which allows you to perform type mapping based on conditional expressions. I will not go into details here, but just leave a link to the documentation and insert a piece of code from it as an illustration:
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"
How this feature helps in our case. Looking at the type description of the react-redux
, we can find the type InferableComponentEnhancerWithProps
, which is responsible for ensuring that the types of injected properties do not fall into the external type of component properties, which we must explicitly specify when instantiating the component. The InferableComponentEnhancerWithProps
type has two general parameters: TInjectedProps
and TNeedsProps
. We are interested in the first. Let's try to "pull" this type of this connector!
type TypeOfConnect<T> = T extends InferableComponentEnhancerWithProps<infer Props, infer _> ? Props : never ;
And directly type pulling on a real example from the repository (which you can clone and run a test program there):
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);
In the example above, we divide the connection to the repository (Redux) into two steps. At the first stage, we obtain a higher-order storeEnhancer
(also InferableComponentEnhancerWithProps
) to retrieve the injected property types from it using our TypeOfConnect
helper TypeOfConnect
and further combining (through the intersection of the &
type types) the property types obtained with the property types of the component properties. In the second stage, we simply decorate our original component. Now, whatever you add to the connector, it will automatically fall into the component property types. Great, what we wanted to achieve!
The attentive reader noted that action generators (for brevity, hereafter, simplify to the action term) with side effects (thunk action creators) are further processed using the unboxThunk
function. What caused such an additional measure? Let's figure it out. First, we look at the signature of such an action on the example of the program from the repository:
const thunkAction = (delay: number): ThunkAction<void, RootStore, void, AnyAction> => (dispatch) => { console.log('waiting for', delay); setTimeout(() => { console.log('reset'); dispatch(reset()); }, delay); };
As can be seen from the signature, our action does not immediately return the target function, but first an intermediate one, which redux-middleware
picks up to make it possible to produce side effects in our main function. But when using this function in the connected form in the component properties, the signature of this function is reduced, excluding the intermediate function. How to describe it in types? Need a special function converter. And again TypeScript shows its power. First we describe the type that removes the intermediate function from the signature:
CutMiddleFunction<T> = T extends (...arg: infer Args) => (...args: any[]) => infer R ? (...arg: Args) => R : never ;
Here, in addition to the conditional types, a completely new innovation from TypeScript 3.0 is used, which allows you to display the type of an arbitrary (rest parameters) number of function arguments. See the documentation for details. It now remains to cut out the extra part from our action in a rather tough way:
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> );
Having skipped the action through such a converter, we have the required signature at the output. Now the action is ready for use in the connector.
So, by simple manipulations, we reduce our manual work when writing typed code on our stack. If you go a little further, you can also simplify the typing of action games and reducers, as we did in redux-modus .
PS When using dynamic linking of actions in the connector via the function and redux.bindActionCreators
we will need to take care of the more correct typing of this utility (perhaps by writing your own wrapper).
Update 0
If this solution seemed convenient to someone, then you can like it here so that the type-utility can be added to the @types/react-redux
.
Update 1
A few more types, with the help of which you do not need to explicitly specify the type of injected hock props. Just take the hoki and pull the types out of them:
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}
Source: https://habr.com/ru/post/431452/
All Articles