Reputation: 253
I was falling in love with TypeScript until I found some very discouraging incompatibilities between Redux-Form with React-Redux.
My goal is wrap a reduxForm
decorated component with the react-redux connect
decorator—this pattern has always worked for me in babel configurations and seems to follow the HOC methodology. Here's an example:
import * as React from 'react';
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
import { reduxForm, Field, InjectedFormProps } from 'redux-form';
interface SampleFormData {
username: string;
}
interface SampleFormProps {
saveData: (data: SampleFormData) => void;
}
type AllSampleFormProps = SampleFormProps & InjectedFormProps<SampleFormData>;
const SampleForm: React.SFC<AllSampleFormProps> = (props) => (
<form onSubmit={props.handleSubmit(props.saveData)}>
<Field name="username" component="input" />
</form>
);
const DecoratedSampleForm = reduxForm<SampleFormData>({ form: "sampleForm" })(SampleForm);
export default connect(
() => ({}),
(dispatch) => ({
saveData: (data: SampleFormData) => dispatch({ type: "SAVE_DATA", data })
})
)(DecoratedSampleForm);
Here's the errors TypeScript is throwing:
> Argument of type 'DecoratedComponentClass<SampleFormData,
> Partial<ConfigProps<SampleFormData, {}>>>' is not assignable to
> parameter of type 'ComponentType<{ saveData: (data: SampleFormData) =>
> { type: string; data: SampleFormData; }; }>'.
>
> Type 'DecoratedComponentClass<SampleFormData,
> Partial<ConfigProps<SampleFormData, {}>>>' is not assignable to type
> 'StatelessComponent<{ saveData: (data: SampleFormData) => { type:
> string; data: SampleFormData; };...'.
>
> Type 'DecoratedComponentClass<SampleFormData,
> Partial<ConfigProps<SampleFormData, {}>>>' provides no match for the
> signature '(props: { saveData: (data: SampleFormData) => { type:
> string; data: SampleFormData; }; } & { children?: ReactNode; },
> context?: any): ReactElement<any>'.
Has anyone found a solution to make react-redux accept the DecoratedComponentClass
type? I found a suggestion to use a "middle" component but I haven't managed to get this to work with thunk actions. Plus I've found that this creates more problems than it solves in terms of typing the form's props.
Upvotes: 9
Views: 4319
Reputation: 3457
A higher Component Interface declaration does the trick that wraps up connects Type with component state and props Type using a decorator.
connect.ts
import * as React from "react";
import {
connect as originalConnect,
MapStateToPropsParam,
MergeProps,
Options
} from "react-redux";
import { IState } from "./index";
export interface IDisPatchProps {
[key: string]: () => void;
}
export type InferableComponentEnhancerWithProps<TInjectedProps, TNeedsProps> = <
TComponent extends React.ComponentType<TInjectedProps & TNeedsProps>
>(
component: TComponent
) => TComponent;
export interface IConnectProps {
<TStateProps = {}, TDispatchProps = {}, TOwnProps = {}>(
mapStateToProps?: MapStateToPropsParam<TStateProps, TOwnProps, IState>,
mapDispatchToProps?: IDisPatchProps
): InferableComponentEnhancerWithProps<
TStateProps & TDispatchProps,
TOwnProps
>;
<TStateProps = {}, TDispatchProps = {}, TOwnProps = {}, TMergedProps = {}>(
mapStateToProps?: MapStateToPropsParam<TStateProps, TOwnProps, IState>,
mapDispatchToProps?: IDisPatchProps,
mergeProps?: MergeProps<
TStateProps,
TDispatchProps,
TOwnProps,
TMergedProps
>,
options?: Options<TStateProps, TOwnProps, TMergedProps>
): InferableComponentEnhancerWithProps<TMergedProps, TOwnProps>;
}
declare module "react-redux" {
// tslint:disable-next-line
interface Connect extends IConnectProps {}
}
export const connect = originalConnect as IConnectProps;
***ClassFile***
@connect(
(state: IState): IStateProps => ({
count: state.counter.count,
isLoading: state.counter.isLoading
}),
{
decrement,
increment
}
)
export default class MyApp
credit goes to: TomasHubelbauer https://github.com/TomasHubelbauer
Upvotes: 1
Reputation: 253
To anyone who comes across this, I found that I was able to dismiss the error by providing the connect statement with empty TStateProps
and TDispatchProps
objects.
interface SampleFormData {
username: string;
}
interface SampleFormProps {
saveData: (data: SampleFormData) => void;
}
type AllSampleFormProps = SampleFormProps & InjectedFormProps<SampleFormData>;
const SampleForm: React.SFC<AllSampleFormProps> = (props) => (
<form onSubmit={props.handleSubmit(props.saveData)}>
<Field name="username" component="input" />
</form>
);
const DecoratedSampleForm = reduxForm<SampleFormData>({ form: "sampleForm" })(SampleForm);
export default connect<{},{}>(
() => ({}),
(dispatch) => ({
saveData: (data: SampleFormData) => dispatch({ type: "SAVE_DATA", data })
})
)(DecoratedSampleForm);
The one downside to this is that it forces us to blindly supply connect props but I felt that this was a more elegant solution than writing an override @types declaration.
To address this shortcoming, I was able to validate the types by providing connect with the correct interfaces versus empty objects; however, this method can only be done temporarily to check the bindings as it doesn't resolve the DecoratedComponentClass
error.
export default connect<{}, SampleFormProps, InjectedFormProps<SampleFormData>>(
() => ({}),
(dispatch) => ({
saveData: (data: SampleFormData) => dispatch({ type: "SAVE_DATA", data })
})
)(DecoratedSampleForm);
Upvotes: 12