loading...

Practical TypeScript. React + Redux

voronar profile image Kirill Alexander Khalitov Updated on ・5 min read

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.

Discussion

pic
Editor guide
Collapse
surgeboris profile image
surgeboris

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 your unboxThunk.

Collapse
voronar profile image
Kirill Alexander Khalitov Author

Thanks. I will watch new built-in types.