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?
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);
This how I do it in a Typescript Redux App (adjusted to your code but not tested)
edited with comment below
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
....
}
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>;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With