22

I'm having some trouble figuring out how to properly type Redux containers.

Consider a simple presentational component that might look like this:

interface MyProps {
  name: string;
  selected: boolean;
  onSelect: (name: string) => void;
}
class MyComponent extends React.Component<MyProps, {}> { }

From the perspective of this component all props are required.

Now I want to write a container that pulls all these props out of state:

function mapStateToProps(state: MyState) {
  return {
    name: state.my.name,
    selected: state.my.selected
  };
}

function mapDispatchToProps(dispatch: IDispatch) {
  return {
    onSelect(name: string) {
      dispatch(mySelectAction(name));
    }
  };
}

const MyContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(MyComponent);

This works, but there's a big typing problem: the mapping functions (mapStateToProps and mapDispatchToProps) have no protection that they are providing the right data to fulfill MyProps. This is prone to error, typos and poor refactoring.

I could make the mapping functions return type MyProps:

function mapStateToProps(state: MyState): MyProps { }

function mapDispatchToProps(dispatch: IDispatch): MyProps { }

However this doesn't work unless I make all MyProp props optional, so that each mapping function can return only the portion they care about. I don't want to make the props optional, because they aren't optional to the presentational component.

Another option is to split up the props for each map function and combine them for the component props:

// MyComponent
interface MyStateProps {
  name: string;
  selected: boolean;
}

interface MyDispatchProps {
  onSelect: (name: string) => void;
}

type MyProps = MyStateProps & MyDispatchProps;

class MyComponent extends React.Component<MyProps, {}> { }

// MyContainer
function mapStateToProps(state: MyState): MyStateProps { }

function mapDispatchToProps(dispatch: IDispatch): MyDispatchProps { }

Ok, that's getting me closer to what I want, but its kind of noisy and its making me write my presentational component prop interface around the shape of a container, which I don't like.

And now a second problem arises. What if I want to put the container inside another component:

<MyContainer />

This gives compile errors that name, selected, and onSelect are all missing... but that's intentional because the container is connecting to Redux and providing those. So this pushes me back to making all component props optional, but I don't like that because they aren't really optional.

Things get worse when MyContainer has some of its own props that it wants to be passed in:

<MyContainer section="somethng" />

Now what I'm trying to do is have section a required prop of MyContainer but not a prop of MyComponent, and name, selected, and onSelect are required props of MyComponent but optional or not props at all of MyContainer. I'm totally at a loss how to express this.

Any guidence on this would be appreciated!

3 Answers 3

22

You're on the right track with your last example. What you also need to define is a MyOwnProps interface, and type the connect function.

With these typings: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/react-redux/react-redux.d.ts, you can do something like this

interface MyProps {
  section: string;
  name: string;
  selected: boolean;
  onSelect: (name: string) => void;
}

interface MyStateProps {
  name: string;
  selected: boolean;
}

interface MyDispatchProps {
  onSelect: (name: string) => void;
}

interface MyOwnProps {
  section: string;
}

class MyComponent extends React.Component<MyProps, {}> { }

function mapStateToProps(state: MyState): MyStateProps { }

function mapDispatchToProps(dispatch: IDispatch): MyDispatchProps { }

const MyContainer = connect<MyStateProps, MyDispatchProps, MyOwnProps>(
  mapStateToProps,
  mapDispatchToProps
)(MyComponent);

This also lets you type ownProps in mapStateToProps and mapDispatchToProps, e.g.

function mapStateToProps(state: MyState, ownProps: MyOwnProps): MyStateProps

You don't need to define an intersection type for MyProps type as long as you define dispatch, state, and ownProps types. That way you can keep MyProps in its own place and let containers, or places where the component is used without containers, apply the props as they need to. I guess this is up to you and your use case - if you define MyProps = MyStateProps & MyDispatchProps & MyOwnProps, it's tied to one specific container, which is less flexible (but less verbose).

As a solution, it is pretty verbose, but I don't think there's any way of getting around telling TypeScript that the different pieces of required props will be assembled in different places, and connect will tie them together.

Also, for what it's worth, I have typically gone with optional props, for simplicity's sake, so I don't have much in the way of experience to share on using this approach.

Sign up to request clarification or add additional context in comments.

10 Comments

Thanks, this helps. I guess without TypeScript everything is optional anyway. I just like that component props can tell you what things are expected vs what things are optional.
Question: since this solution basically means you define each prop twice (once in MyProps, and once in the appropriate MyStateProps or MyDispatchProps), what happens if you rename or change a prop in MyProps, such as name to id or selected: boolean to selected: SelectionEnum? Does the type checker realize that MyOwnProps, MyDispatchProps, MyStateProps doesn't satisfy MyProps anymore?
Somewhat. I've been playing around with it since writing the answer. State and Dispatch props have to match, but OwnProps don't have to match. I guess maybe this is because it's possible to use ownProps only in mapStateToProps and mapDispatchToProps and not use them in MyComponent?
The error is like (123,3): error TS2345: Argument of type 'typeof FeatureTab' is not assignable to parameter of type 'ComponentClass<IStateToProps & IDispatchToProps> | StatelessComponent<IStateToProps & IDispatchTo...'...
Ha, I don't think I'd know what that error is saying... does it trickle down to something helpful like 'name' exists in {...} but not in {...}?
|
3

I just use Partial<MyProps>, where Partial is a built-in TypeScript type defined as:

type Partial<T> = {
    [P in keyof T]?: T[P];
}

It takes an interface and makes every property in it optional.

Here's an example of a presentational/Redux-aware component pair I've written:

/components/ConnectionPane/ConnectionPane.tsx

export interface IConnectionPaneProps {
  connecting: boolean;
  connected: boolean;
  onConnect: (hostname: string, port: number) => void;
}

interface IState {
  hostname: string;
  port?: number;
}

export default class ConnectionPane extends React.Component<IConnectionPaneProps, IState> {
   ...
}

/containers/ConnectionPane/ConnectionPane.ts

import {connect} from 'react-redux';
import {connectionSelectors as selectors} from '../../../state/ducks';
import {connect as connectToTestEcho} from '../../../state/ducks/connection';
import {ConnectionPane, IConnectionPaneProps} from '../../components';

function mapStateToProps (state): Partial<IConnectionPaneProps> {
  return {
    connecting: selectors.isConnecting(state),
    connected: selectors.isConnected(state)
  };
}

function mapDispatchToProps (dispatch): Partial<IConnectionPaneProps> {
  return {
    onConnect: (hostname, port) => dispatch(connectToTestEcho(hostname, port))
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(ConnectionPane) as any;

The presentational component props are non-optional - exactly as required for the presentation without any bearing on the corresponding "smart" component.

Meanwhile, mapStateToProps and mapDispatchToProps will allow me to assign a subset of the required presentational props in each function, while flagging any props not defined in the presentational props interface as an error.

6 Comments

Yeah, since the TS release with Partial support I've been using this on some projects. It's easy to use. It's not as strict as the formal StateProps, DispatchProps and OwnProps approach because it doesn't ensure a mapping function is complete, only that it doesn't return an incorrectly implemented prop. I've toyed with using Pick to pry apart a single presentational component props interface but that's still pretty tedious.
Making everything optional is not the solution as the question was asking clearly against it.
@wegginho OP wrote "However this doesn't work unless I make all MyProp props optional, so that each mapping function can return only the portion they care about. I don't want to make the props [MyProp] optional". My suggestion is not to make MyProp optional or indeed to make any change at all to the presentational component or its interface. It's to wrap MyProp in a type that exposes all of its properties as optional, only for the Redux typings. This is the ideal solution IMO as it allows each Redux function to handle a subset of props, while safeguarding against unknown props.
@Tagc is it complaining if you're not passing in required props?
Wegginho is right that this isn't quite the ideal solution I was looking for because it does make all the props optional for the mapping functions, ie map functions aren't ensured to return everything they are supposed to. However it is quite a bit better than explicitly making the component props all optional and it is good alternative that's easy to use so I upvoted it. :)
|
2
interface MyStateProps {
    name: string;
    selected: boolean;
}

interface MyDispatchProps {
    onSelect: (name: string) => void;
}

interface MyOwnProps {
    section: string;
}

// Intersection Types
type MyProps = MyStateProps & MyDispatchProps & MyOwnProps;


class MyComponent extends React.Component<MyProps, {}> { }

function mapStateToProps(state: MyState): MyStateProps { }

function mapDispatchToProps(dispatch: IDispatch): MyDispatchProps { }

const MyContainer = connect<MyStateProps, MyDispatchProps, MyOwnProps>(
  mapStateToProps,
  mapDispatchToProps
)(MyComponent);

You can use something use called Intersection Types https://www.typescriptlang.org/docs/handbook/advanced-types.html#intersection-types

1 Comment

Thanks for this example. It's basically a combination of my original attempt (using intersection types) and @radio's accepted answer (using the connect() type args).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.