published on October 28th, 2017
One of the challenges when trying to use redux with TypeScript in a
type safe way is to use the react-redux connect
function in a type
safe manner. To the best of my knowledge, it was previously not possible
to write a type definition for TypeScript which would catch property-related
type errors. The main culprit was TypeScripts unsound handeling of function
argument types, which were bivariant when they should have been contravariant
instead.
However, with TypeScript 2.6 a new strictness option is included called
strictFunctionTypes
. This option will ensure that function arguments are
typed contravariantly, as they should be. It turns out that this is really
all that is needed to make the existing type definition of the connect
function
type safe.
The redux connect
function takes a plain react component which only
depends on its properties and connects it to a redux store, making it
a container which can depend on and modify the application state in the
redux store.
To do this, connect
takes two functions, mapStateToProps
and
mapDispatchToProps
. As the name suggests, mapStateToProps
uses the
current application state to produce properties and the mapDispatchToProps
function uses the dispatch function to produce properties for the component
to be connnected. Generally, the first will produce properties for the component
to read and the second will produce callbacks for the component to modify
the application state. The merged return values from mapStateToProps
and mapDispatchToProps
will be passed to the component to be connected.
Thus, the type definition of the connect
function should ensure that
the merged return values are a superset of the properties that the
component passed to the connect function expects.
Up to TypeScript 2.5, function arguments would always be typed bivariantely,
which would lead to the following problem with the connect
function:
interface OldProps {
oldIndex: number;
}
const OldComp: React.SFC<OldProps> = props => {
return div(undefined, props.oldIndex.toString());
};
interface NewProps {
newIndex: number;
}
interface State {}
interface Dispatch {}
interface PropsFromState {}
interface PropsFromDispatch {}
function mapStateToProps(s: State, np: NewProps): PropsFromState {
return {};
}
function mapDispatchToProps(d: Dispatch, np: NewProps): PropsFromDispatch {
return {};
}
// No type error in TypeScript <= 2.5 - but using NewComp leads to a
// runtime error as `oldIndex` is undefined
const NewComp = connect(mapStateToProps, mapDispatchToProps)(OldComp);
The problem here is that PropsFromState & PropsFromDispatch = {} & {} = {}
is clearly not a superset of OldProps = { oldIndex: number; }
, but the
type checker (up to 2.5) does not complain about this as it is happy if
either (PropsFromState & PropsFromDispatch) extends OldProps
or OldProps extends (PropsFromState & PropsFromDispatch)
. This is just the unsoundness which
is introduced by typing function arguments bivariantly.
With the strictFunctionTypes
option enabled, TypeScript 2.6 now types
function arguments contravariantly. In case of the connect function this means
that it insists that (PropsFromState & PropsFromDispatch) extends OldProps
must hold, the other way around is no longer enough. This fixes the problem
pointed out above and makes the type definition of the connect function
type safe.