React, Redux and TypeScript: Typed Connect

Posted on 2017-03-09 00:06:38+00:00

React and Redux go well together. And thanks to a lot of hard work by the TypeScript team, it's become much easier to use the React/Redux ecosystem in a strongly-typed fashion. From discriminated unions to partial types you can practically type every part of your app. And yet typing connect from react-redux is still elusive. Read on to find out how.

Yes, there are typings out there, but in my experience, the generic typings out there all require you to pass in your typed State left and right. That's simply too much boilerplate to be useful. At least to me. And the typing we'll use is so simple that maintenance is trivial.

Learning TypeScript? Subscribe to my TypeScript articles, tips and tutorials.

Let's get started.

First, we define a few basic types we use all over our apps. There are much better ways to type State and Action, but I just typed them inline for the purposes of this post.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type State = {
    auth: {
        isLoggedIn: boolean,
    },
};

type Action = {
    type: string,
    [name: string]: any,
};

Then we type Dispatch. Bonus: we add support for thunks. So when you dispatch a thunk, you'll get back the thunk's return value. When you dispatch a plain action, you get back void.

1
2
3
4
5
6
7
export interface Dispatch {
  <R>(asyncAction: (dispatch: Dispatch, getState: () => State) => R): R;
  <R>(asyncAction: (dispatch: Dispatch) => R): R;
  // (neverAction: (dispatch: Dispatch, getState: () => GetState) => never): never;
  (action: Action): void;
  // (action: Thunk): ; // thunks in this app must return a promise
}

Finally, we create our typed connect:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import * as React from 'react';
import {connect} from 'react-redux';

// We use generic inference.
function typedConnect<OwnProps, StateProps, DispatchProps>(
    // And "capture" the return of mapStateToProps
    mapStateToProps: (state: State, ownProps: OwnProps) => StateProps,
    // As well as the return of mapDispatchToProps.
    // Or in case you use the shorthand literal syntax, capture it as is.
    mapDispatchToProps?: DispatchProps | ((dispatch: Dispatch, ownProps: OwnProps) => DispatchProps),
) {
    // We combine all generics into the inline component we'll declare.
    return function componentImplementation(component: React.StatelessComponent<OwnProps & StateProps & DispatchProps>) {
        // Finally, we double assert the real connect to let us do anything we want.
        // And export a component that only takes OwnProps.
        return connect(mapStateToProps, mapDispatchToProps as any)(component) as any as React.StatelessComponent<OwnProps>;
    }
}

So how do you use it? As a inline stateless component. That's what makes this whole chicken dance work: with the community-typed connect definitions, you lack proper inference and have to repeat yourself often. But by using a stateless component defined inline, you get all the inference you need and only the props that you actually have. Better yet: the connected component will not require that you pass in the props that come from redux.

Below is a real life example. We want an <App /> component that expects one prop to be passed in isDebugging. All other props should come from connect.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
interface Props {
    isDebugging: boolean;
}

const mapStateToProps = (state: State, ownProps: Props) => ({
    isReady: state.boot.isReady,
    isMenuOpen: state.scenes.isMenuOpen,
    isAuthenticated: isAuthenticated(state),
    profile: getProfile(state),
    managedProfile: getCurrentlyManagedProfile(state),
    title: state.scenes.title,
    isDelegatable: state.scenes.isDelegatable,
    isLoading: getIsLoading(state),
    hasHelpToShow: state.scenes.help != null,
    useKilojoules: getUseKilojoules(state),
    isAskingForRating: state.scenes.isAskingForRating as typeof state['scenes']['isAskingForRating'],
});

const mapDispatchToProps = {
    clearManagedProfile,
};

const App = typedConnect(mapStateToProps, mapDispatchToProps)(props => <div className={styles.app}>
    <Notifications />
    <Menu {...props} />
    <Dialogs />
    <div className={styles.viewport} onClick={closeIfMenuOpen(props.isMenuOpen, props.toggleMenu)}>
        <Navigation {...props} showHelp={props.hasHelpToShow ? props.showHelp : null} />

        {props.managedProfile != null && props.isDelegatable &&
        <ManagerBar clearManagedProfile={props.clearManagedProfile} managedProfile={props.managedProfile} />
        }

        {props.isAskingForRating !== false &&
        <RatingRequest {...props} isAskingForRating={props.isAskingForRating} />
        }

        {props.isReady &&
        <div className={styles.main}>
            {props.children}
        </div>
        }
    </div>
</div>);

This component has a ton of props that come in from the global redux state. But the connected component need only be passed isDebugging as a prop. Nothing else. In fact, it won't let you pass in anything.

Nice, simple, and avoids repetition.

Stay tuned for a future post showing more advanced techniques, such as exporting undecorated versions for easy testing.

Comments

krawaller2017-04-28 06:20:46+00:00

Thank you ever so much for this post, Silvio! I was battling the exact same puzzle, to the point of also trying to create a typed version of .connect, but I couldn't bring it all the way to the finish line. Piggybacking on your hard work was the solution, thank you for sharing! :)

Reply
silviogutierrez2017-04-30 16:02:24+00:00

Thanks for your kind words. I'm glad this was helpful.

Reply
krawaller2017-05-13 04:56:54+00:00

Did you see this turning up? https://github.com/Microsoft/TypeScript-React-Starter

Here we get fully typed connected components without having to jump through hoops, using only typings! :)

Reply
silviogutierrez2017-05-13 05:11:16+00:00

I did check that out today, actually. But as you can see, the amount of non-inferred typing is pretty excessive. They basically declare everything at least twice.

For now I'll stick to my approach, but hopefully that repo will evolve over time.

Reply
krawaller2017-05-13 05:54:33+00:00

I found it super clean to use: https://gist.github.com/krawaller/f0d12beb6a5593b10614e96455080ec3

...but only now realised that when you mix in props from the parents they no longer infer state and dispatch, instead as you say you have to shove everything in, again. I see what you mean.

Indeed, let's hope it matures!

Reply
Marcel Veldhuizen2017-05-05 12:53:30+00:00

I came up with a similar solution, but I'm not quite happy with my solution for more complex components that you would typically implement by extending React.Component<T, S>.

The best I've come up with is something like this:


const { propsGeneric, connect } =
    connectedComponentHelper<Props>()(mapStateToProps, mapDispatchToProps);
type ComponentProps = typeof propsGeneric;

class MyComponent extends React.Component<ComponentProps, {}> {
    // Component implementation
}

The implementation of the helper method can be found in my blog post.

Do you have any ideas to maybe simplify this further?

Reply
silviogutierrez2017-05-13 16:03:12+00:00

Your approach is close to the method I use for class based implementations. Mine has a touch less typing, but ultimately you do need that "fake" propsGeneric variable so you can run typeof on it.

Reply
stevematdavies2017-11-21 12:23:37+00:00

How does this even work??? mapDispatchToProps state is typed as your state, however, in redux, the state will also include the name of the reducer function, which is not on your State Object.

Reply
silviogutierrez2017-11-30 18:20:05+00:00

Hi there,

Not sure what you're referring to. The name of the reducer function is never included in any object passed to the map functions. Do you mind elaborating?

Best,

Silvio

Reply
∆ [c0d3r28] ∆2017-12-01 06:55:09+00:00

With combined reducers, the exported reducer function name becomes part of the state tree, and thus has to be reflected in any state typing.

Reply
silviogutierrez2017-12-01 15:48:53+00:00

I see what you mean. In the above case, "auth" is one of the reducers that then gets combined into the single main reducer. So you can see it's reflected in the typing for State.

In reality, I generate this State typing like so:

export interface State { auth: typeof auth.initialState; boot: typeof boot.initialState; }

You can see that each sub-reducer exports its initial state and we can combine it into the State typing.

Reply
stevematdavies2017-12-01 15:57:53+00:00

I like that approach, but in reality it will add complexity to the code base surely, as dont' you have to also define all those initial states, and in some cases the initial state values are null or empty objects, which can also need typing themselves?

For example, in a theming module I am doing, the customization initial state is simply an empty Object, but once its populated, it becomes a nested object of other objects, each would also need there own type to compile. Herein lies the issue.

´ ìniitalState= { custom: {}} // customizationReducer

// after populating

// customization Reducer state (for example) { ... other things custom: { palette: {...}, themes: {[// themeTypes]}, ids: [], text: {[{//text-types},{},{}] }}} } ` So You can see the complexity, it wont just be able to mapToState and say:

theme: typeof customization.initialState unless the initialState itself is fully typed

Reply
silviogutierrez2017-12-01 16:00:54+00:00

In my case, 99% of my inner initialStates are object literals so typeof just works nicely. Example from boot.ts:

export const initialState = { isReady: false, version: '', platform: null as 'web'|'android'|'ios'|null, };

Reply

Post New Comment