Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React redux typescript: connect has missing type error

I am trying to get a react-redux app going with typescript, but I keep circling around the same error. The following code compiles and produces expected result

// State definition
interface HelloWorldState {
    clickCount: number
}
interface AppState extends HelloWorldState {}


// Props definitions
interface HelloWorldProps {
    count: number
}


// Actions
const CLICK = 'CLICK';
const click = () => {return {type:CLICK}};


// Reducers
function clickCount(state:number = 0, action:Action) {
    if (typeof state === 'undefined') {
        return 0;
    }
    switch (action.type) {
        case CLICK:
            return state + 1;
        default:
            return state;
    }
}
let rootReducer = combineReducers({
    clickCount
});


// Store
let store = createStore(rootReducer);


// Components
class HelloWorld extends React.Component<any, any> {
    render() {
        return <div onClick={this.handleClick.bind(this)}>Hello world "{this.props.count}"</div>
    }

    handleClick() {
        store.dispatch(click())
    }
}


// Container components
const mapStateToProps = (state:AppState):HelloWorldState => {
    return Immutable.fromJS({
        count: state.clickCount
    })
};
const ConnectedHelloWorld = connect(
    mapStateToProps
)(HelloWorld);

render(
    <Provider store={store}>
        <ConnectedHelloWorld/>
    </Provider>,
    container
);

Great! But I am using TypeScript, because I want to get type checks at compile time. The most important thing to type check is state and props. So instead of class HelloWorld extends React.Component<any, any>, I want to do class HelloWorld extends React.Component<HelloWorldProps, any>. When I do this, however, I get the following compile error from the call to render

TS2324:Property 'count' is missing in type 'IntrinsicAttributes & IntrinsicClassAttributes<HelloWorld> & HelloWorldProps & { children?: React...'.

I don't really understand why. count IS present in the HelloWordProps definition, and it is provided by the reducer, so I should be fine, right? Similar questions has suggested that it is an inference problem, and that I should declare the type of the call to connect, but I can't seem to find out how

package.json

{
  "name": "reacttsx",
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "ts-loader": "^1.3.3",
    "typescript": "^2.1.5",
    "webpack": "^1.14.0",
    "typings": "^2.1.0"
  },
  "dependencies": {
    "es6-promise": "^4.0.5",
    "flux": "^3.1.2",
    "immutable": "^3.8.1",
    "isomorphic-fetch": "^2.2.1",
    "jquery": "^3.1.1",
    "react": "^15.4.2",
    "react-dom": "^15.4.2",
    "react-redux": "^5.0.2",
    "redux": "^3.6.0",
    "redux-logger": "^2.7.4",
    "redux-thunk": "^2.2.0"
  }
}

typings.json

{
  "dependencies": {
    "flux": "registry:npm/flux#2.1.1+20160601175240",
    "immutable": "registry:npm/immutable#3.7.6+20160411060006",
    "react": "registry:npm/react#15.0.1+20170104200836",
    "react-dom": "registry:npm/react-dom#15.0.1+20160826174104",
    "react-redux": "registry:npm/react-redux#4.4.0+20160614222153",
    "redux-logger": "registry:dt/redux-logger#2.6.0+20160726205300",
    "redux-thunk": "registry:npm/redux-thunk#2.0.0+20160525185520"
  },
  "globalDependencies": {
    "es6-promise": "registry:dt/es6-promise#0.0.0+20160726191732",
    "isomorphic-fetch": "registry:dt/isomorphic-fetch#0.0.0+20170120045107",
    "jquery": "registry:dt/jquery#1.10.0+20170104155652",
    "redux": "registry:dt/redux#3.5.2+20160703092728",
    "redux-thunk": "registry:dt/redux-thunk#2.1.0+20160703120921"
  }
}

UPDATE

Since it was complaining that count was missing, I tried updating to

render(
    <Provider store={store}>
        <ConnectedHelloWorld count={0}/>
    </Provider>,
    container
);

This solves the issue. So the issue is that the compiler doesn't know that the Provider is providing the count. Provider uses the store. The store should have the clickCount value which is mapped to count by the container component.

I noticed I'd forgotten an initial state for the store. So even if the types had checked out, the state would be empty. I updated it to

// Store
let initialState:AppState = {clickCount: 0};
let store = createStore(rootReducer, initialState);

Now I am certain that clickCount is set properly in the store. So I'd expect the mapStateToProps function to take the AppState and return the HelloWorldProps as specified, and then the Provider should provide the count value. This is true, but the compiler does not see it.

So how to remedy that?

like image 814
Eldamir Avatar asked Jan 23 '17 14:01

Eldamir


2 Answers

In my case, I was passing null for the mapDispatchToProps param in the connect function like this since I wasn't using dispatch for this component:

export default connect(mapStateToProps, null)(MainLayout);

Changing it to just omit the mapDispatchToProps param fixed it for me

export default connect(mapStateToProps)(MainLayout);

like image 170
Alexandrith Sharron Avatar answered Sep 28 '22 07:09

Alexandrith Sharron


This how I do it in a Typescript Redux App (adjusted to your code but not tested)

edited with comment below

  1. type connect with props for the Connected Component (ConnectedHelloWorldProps)

    const ConnectedHelloWorld:React.ComponentClass<ConnectedHelloWorldProps>  = 
        connect<any,any,HelloWorldProps>(mapStateToProps)(HelloWorld)
    
    interface ConnectedHelloWorldProps { }
    
    interface HelloWorldProps extends ConnectedHelloWorldProps {
        count: number
        ....
    }
    
  2. use the connected component and its ConnectedHelloWorldProps props in the Provider

    <Provider store={store}>
        <ConnectedHelloWorld/>
    </Provider>
    

Note: this works fine with these typings

"@types/react": "^0.14.52",
"@types/react-dom": "^0.14.19",
"@types/react-redux": "^4.4.35",
"@types/redux-thunk": "^2.1.32",

ConnectedHellowWorldProps is not really needed here, since it is an empty interface, but in a real world scenario it is likely to contain a few props.

The basic principle is this: ConnectedHelloWorldProps contain what needs to be passed at the Provider level. In mapStateToProps and/or mapDispatchToProps, enrich the actual Component HelloWorldProps with whatever is needed

Redux Typescript typings are a beast but what is shown above should be sufficient.

export declare function connect<TStateProps, TDispatchProps, TOwnProps>(
mapStateToProps: FuncOrSelf<MapStateToProps<TStateProps, TOwnProps>>,
 mapDispatchToProps?: FuncOrSelf<MapDispatchToPropsFunction<TDispatchProps, TOwnProps> | MapDispatchToPropsObject>): ComponentDecorator<TStateProps & TDispatchProps, TOwnProps>;


 interface ComponentDecorator<TOriginalProps, TOwnProps> {
    (component: ComponentClass<TOriginalProps> | StatelessComponent<TOriginalProps>): ComponentClass<TOwnProps>;
 }
like image 24
Bruno Grieder Avatar answered Sep 28 '22 07:09

Bruno Grieder