Preface
Today any modern front-end development process that more complex than hello world
application, where different teams works under the one project, makes high demands to code quality. In order to hold the high quality code in our #gostgroup
front-end team we keep up with the times and don't afraid use modern technologies that show their practical benefits on the example of some projects of different size companies.
There are many articles about benefits of a static typing on the example of TypeScript, but today we focus on more practical issues from our favorite (in #gostgroup
, I think your too) front-end stack (React + Redux).
"I don't know how do you live at all without a strong static typing. What do you do? Debug your code all day?" - unknown person.
"No, we write types all day." - my colleague.
Many people complain to a fact what writing code in TypeScript (here and next I mean subject stack) enforce you to spend much time on coding types manually. A good example of this is connect
function from react-redux
library:
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? As you can see for every new injected property passed via connector we must declare the property type in common React component property types. Very boring stuff. It is would be a cool thing if we have a possibility to automatically merge all connector injected property types in to one general type and just join this type with common React component property types. I have good news for you. Right now we able to do this awesome typing with TypeScript. Ready? Go!
Super force of TypeScript
TypeScript doesn't stagnate for a long time and rapidly progress (I like it very much). Beginning from a version of 2.8 we got very interesting feature (conditional types) that allow us to "to express non-uniform type mappings". I will not be stopping here for giving you deep explanation about this feature and just leave a link to the documentation with example from this one:
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"
And how this feature can help us with our problem? In react-redux
library typings there is InferableComponentEnhancerWithProps
type that hides injected property types from public component interface (properties that we must pass explicitly when we instantiate a component via JSX
). InferableComponentEnhancerWithProps
type has two generic type parameters: TInjectedProps
and TNeedsProps
. We interested in first one. Let's try to "pull" this type from real connector!
type TypeOfConnect<T> = T extends InferableComponentEnhancerWithProps<infer Props, infer _>
? Props
: never
;
And let me show a real working example from repository:
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 example above we divide connection to store in two phases. In first one we assign redux store enhancer to storeEnhancer
variable (it has InferableComponentEnhancerWithProps
type) for extracting injected property types with our TypeOfConnect
type-helper and just join inferred type with own component property types via intersection operator &
. In second phase we decorate our source component. Now whatever property you add to connector it will always be in our component property types. Awesome! It is all we wanted to achieve!
Mind-coder noticed that thunk actions wrapped with special unboxThunk
function. Why we done this? Let's puzzle out this thing. First of all let's see thunk action signature of tutorial application from repo:
const thunkAction = (delay: number): ThunkAction<void, RootStore, void, AnyAction> => (dispatch) => {
console.log('waiting for', delay);
setTimeout(() => {
console.log('reset');
dispatch(reset());
}, delay);
};
As we can see in function signature, thunk action doesn't immediately return main action body, but it returns special function for redux middleware dispatcher. It is common way to make side effects in redux actions. However when we use a bound version of this action in component it has "cutted" form without middle function. How to declare this changeable function signature? We need a special transformer. And again TypeScript shows us his super force. For a start let's declare a type that cut of middle function from any function signature:
CutMiddleFunction<T> = T extends (...arg: infer Args) => (...args: any[]) => infer R
? (...arg: Args) => R
: never
;
Here we use another cool newcomer from TypeScript 3.0 that allow us to infer function rest parameter types (for more detail see the documentation). Next we can define (with little hard type assertion) our "function-cutter":
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>
);
And now we only have to wrap out our source thunk action with this transformer and use it in connector.
In such a simple way we reduce our manual work with types. If you want to go deeper you could try redux-modus library that simplify action and reducer creation in type safety way.
P.S When you will try to use actions binding util like redux.bindActionCreators
you will have to take care of more right type inference that not working out of the box.
Update 0
If someone liked this solution you could make thumb up to see this utility type in @types/react-redux
package.
Update 1
Some useful util types. No more need to manually declare injected property types for hoc's. Just give you hoc and extract its injected property types automatically:
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}
Update2
Merged to react-redux
upstream code base in form of ConnectedProps type.
Top comments (2)
You're using explicit type-casting for thunks in
connect
function (which is not bad per se, it just feels like a hacky workaround to me).@types/react-redux@6.0.11
actually includes higher-order-type similar to your CutMiddleFunction and provides a higher-order-type ResolveThunks that servers a purpose similar to yourunboxThunk
.Thanks. I will watch new built-in types.