4

I've been struggling with this for the longest time. Sat down today dedicated to get rid of any anys left, but didn't manage to.

import * as React from 'react';
import * as Redux from 'redux';
import { connect } from 'react-redux';
import { ReduxState } from './types';
import { syncItem, displayAlert } from './actionCreators';
import { SyncItemAction, DisplayAlertAction } from './actions';

// Props coming from Redux using `connect` and `mapStateToProps`
type AppData = {
    isSelected: boolean;
    isInEditMode: boolean;
};

// Action creators from `./actionCreators` all wrapped in `dispatch` using `connect` and `mapDispatchToProps`
type AppActions = {
    syncItem: (id: number) => SyncItemAction;
    displayAlert: (text: string) => DisplayAlertAction;
};

// Actual JSX attributes that will be required by the type system.
type AppProps = {
    id: number;
    name: string;
} & Partial<AppData> & Partial<AppActions>; // Making data and actions partial so that using <App /> in JSX doesn't yell.

// The component's inner state.
type AppState = Partial<{
    temp: string;
}>;

@connect<AppData, AppActions, AppProps>(mapStateToProps, mapDispatchToProps)(App) // Or mapDispatchToPropsAlt
export default class App extends React.Component<AppProps, AppState> {
    constructor(props: AppProps) {
        super(props);
    }

    render() {
        return (
            <div>
                <h1>Hello, {this.props.name}! (#{this.props.id})</h1>
                {/* In the below, syncItem should take the new name, a detail… Also ID could be provided in `mapStateToProps` by using `ownProps`! */}
                Rename: <input value={this.state.temp} onChange={event => this.setState({ temp: event.target.value })} />
                <button onClick={_ => this.props.syncItem(this.props.id)}>Sync</button>
            </div>
        );
    }
}

function mapStateToProps(state: ReduxState, ownProps?: AppProps): AppData {
    return {
        isSelected: ownProps.id === state.selectedId,
        isInEditMode: state.isInEditMode
    };
}

function mapDispatchToProps(dispatch: Redux.Dispatch<ReduxState>, ownProps?: AppProps): AppActions {
    return {
        syncItem: (id: number) => dispatch(syncItem(id)),
        displayAlert: (text: string) => dispatch(displayAlert(text, ownProps.name))
    };
}

function mapDispatchToPropsAlt(dispatch: Redux.Dispatch<ReduxState>, ownProps?: AppProps): AppActions {
    return {
        syncItem,
        // Making this `null` because `displayAlert` above changes the signature by hiding the other parametr and taking it from `ownProps` - uncommon!
        displayAlert: null
    };
}

function Test() {
    // Only `id` and `name` is correctly required.
    return <App id={0} name={'test'} />;
}

In the code above I get the following:

index.tsx(31,78): error TS2345: Argument of type 'typeof App' is not assignable to parameter of type 'ComponentClass<AppData & AppActions> | StatelessComponent<AppData & AppActions>'.
  Type 'typeof App' is not assignable to type 'StatelessComponent<AppData & AppActions>'.
    Type 'typeof App' provides no match for the signature '(props: AppData & AppActions & { children?: ReactNode; }, context?: any): ReactElement<any>'

I've used the type definitions in node_modules/@types to come up with what I have above and similarly I've checked what ComponentClass<T> looks like. It suggest (it seems to me) that state of the component must be {} | void which I don't understand why it is the case and also if I change the code above to say <AppProps, void> or <AppProps, {}> it doesn't change the first error in any way.

How should I go about this?

syncItem is just function syncItem(id: number): SyncItemAction and SyncItemAction is interface SyncItemAction extends Redux.Action { id: number; }, similarly with displayAlert.

Edit: Found a related question but this one doesn't answer how state can be typed, too.

1 Answer 1

1

This answer does not use the annotation (and uses interfaces rather than types). The basic idea is to completely separate the Connected component and its props from the underlying component (typings wise).

Create a class ConnectedAppProps which are the properties of the connected component you are going to expose to the Provider

interface ConnectedAppProps {
    id: number;
    name: string;
}

extend AppProps from that interface

interface AppProps extends ConnectedAppProps, AppData, AppActions {
}

Create the ConnectedComponent

const ConnectedApp: React.ComponentClass<ConnectedAppProps> = 
    connect<AppData,AppActions,AppProps>( mapStateToProps, mapDispatchToProps )( App )

Use the connected component under the Provider with its ConnectedAppProps

   <Provider store={store}>
       <ConnectedApp id={0} name={'test'} />
   </Provider>

Use mapStateToProp and mapDispatchToPropsas you dot today to "enrich" the component AppProps from those of the ConnectedAppProps

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

Comments

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.