7

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?

5
  • 1
    Do you get this error even if you only do the simple HelloWorld class? (that is, without the redux) Because it works for me. Also, what version of react are you using? Commented Jan 23, 2017 at 15:24
  • By the simple one, you mean HelloWorld extends React.Component<any, any>? Yes, that works always, because the types of its state and props are any. So no error there. Commented Feb 1, 2017 at 6:48
  • No, by simple I mean having only class HelloWorld extends React.Component<HelloWorldProps, any> without the redux stuff. For me that compiles just fine. What version of react/typescript are you using? Commented Feb 1, 2017 at 8:07
  • I updated the question. You are correct, the issue is from redux or react-redux Commented Feb 1, 2017 at 9:46
  • You can find an example of a connect here: github.com/Black-Monolith/LightCycle/blob/master/app/containers/…. It separates the current component from the container file where we connect() the component to the store. We also export MappedProps type to be able to use it as props type in the component. Commented Feb 1, 2017 at 11:41

4 Answers 4

11

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);

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

Comments

7
+100

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>;
 }

8 Comments

will that let you define the <ConnectedHelloWorld /> component without props? They should be provided by the provider after all? When I do <ConnectedHelloWorld count={null} />, the compiler accepts it, and when I run it, I see that the provider provides the 0 from the initial state properly
Yes. Do not define the count property in ConnectedHelloWorldProps and assign it to 0 or state.clickCount in mapStateToProp()
Your call to connect gives me error TS2346: Supplied parameters do not match any signature of call target.
We probably do not have the exact same typings. I have edited the answer with the typings I use
If you use last version of TypeScript you can use NPM to install typings through @types : blogs.msdn.microsoft.com/typescript/2016/06/15/…
|
2

Your problem is that HelloWorldState is defined like this

interface HelloWorldState {
  clickCount: number
}

And the props you want to return from mapStateToProps are

{ 
  count: state.clickCount
}

But you override the return-type as HelloWorldState, so the return type does not contain count but clickCount.


fromJS breaks type safety

ImmutableJS.fromJS works pretty bad with TypeScript type inference:

const state = Immutable.fromJS({
  count: 0
})

Here state is of type any so you don't have any error when assigning it as a return value of type HelloWorldState.


mapStateToProps should return a simple object

mapStateToProps can return a simple object, as you won't directly edit this state from the component:

const mapStateToProps = (state: AppState): HelloWorldState => {
  return {
    count: state.clickCount
  }
}

Here you will have the error you did not have when using ImmutableJS, telling you that you cannot assign { count: number } to { clickCount: number }.

So just remove the return type, and type-inference will do the job, or add the correct type.

const mapStateToProps = (state: AppState) => {
  return {
    count: state.clickCount
  }
}

Statically-typed tree structural-sharing with Monolite

I also recommend you to use Monolite which is a simple set of functions written in TypeScript and designed to be used with Redux states.

It allows you to also define your state with simple JavaScript objects, and to make updates on the state through simple functions.

import { set } from 'monolite'

const state = {
  clickCount: 0
}

const newState = set(state, _ => _.clickCount)(value => value + 1)

P.S. I'm the author of Monolite

Comments

1

I had a similar problem while writing a component class with connect(). In my case, there was a circular dependency in the props

const mapDispatchToProps = (dispatch: Dispatch) => {/* return something here */}
const mapStateToProps = (state: IState, ownProps: AllProps) => {/* return something here */}
type AllProps = {
someExtraProp: string;
} & ReturnType<typeof mapDispatchToProps> & ReturnType<typeof mapStateToProps>;

As you can see this caused a circular type dependency between AllProps and mapStateToProps. I segregated out the OwnProps code and it looks like this now:

const mapDispatchToProps = (dispatch: Dispatch) => {/* return something here */}
const mapStateToProps = (state: IState, ownProps: OwnProps) => {/* return something here */}
const OwnProps = {
someExtraProp: string;
};
type AllProps = OwnProps & ReturnType<typeof mapDispatchToProps> & ReturnType<typeof mapStateToProps>;

Now, the connect call doesn't throw any type errors. Hopefully this helps someone with a similar issue.

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.