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!
You can use something use called Intersection Types https://www.typescriptlang.org/docs/handbook/advanced-types.html#intersection-types
I just use
Partial<MyProps>
, wherePartial
is a built-in TypeScript type defined as: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
/containers/ConnectionPane/ConnectionPane.ts
The presentational component props are non-optional - exactly as required for the presentation without any bearing on the corresponding "smart" component.
Meanwhile,
mapStateToProps
andmapDispatchToProps
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.You're on the right track with your last example. What you also need to define is a
MyOwnProps
interface, and type theconnect
function.With these typings: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/react-redux/react-redux.d.ts, you can do something like this
This also lets you type
ownProps
inmapStateToProps
andmapDispatchToProps
, e.g.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.