The problem with Mobx and TypeScript The official solution to support TypeScript sucks. They are basically asking you to make stores optional in your Prop types:
class MyComponent extends React .Component < { userStore?: IUserStore; otherProp: number }, {}> { }
then use a bang to tell the compiler your stores are present:
public render () { const {a, b} = this .store! }
This is truly inelegant. Below you will find a much better setup to have Mobx and React play nice with TypeScript.
TLDR; import React from 'react' ;import { inject, IWrappedComponent, observer } from 'mobx-react' ;import { MyStoreClass } from './myStore' ;type StoreProps = { myStore: MyStoreClass; }; interface Props extends StoreProps { realProp: string; } @inject('myStore' ) @observer class App extends React .Component <Props > { static defaultProps = {} as StoreProps; render() { const { myStore, realProp } = this .props; [...] } } export default App as typeof App & IWrappedComponent<Props>;
Working Example
Explanations and Steps to convert your code Consider the following example, a simple counter app with a store injected through the context .
import React from 'react' ;import { render } from 'react-dom' ;import { Provider } from 'mobx-react' ;import counterStore from './counterStore' ;import App from './App' ;const rootElement = document .getElementById('root' );render( <Provider counterStore={counterStore}> <App title='normal prop title' /> </Provider>, rootElement ); import { observable } from 'mobx' ;export class CounterStore { @observable counter = 0 ; increment() { this .counter++; } decrement() { this .counter--; } } export default new CounterStore();import React from 'react' ;import { inject, observer } from 'mobx-react' ;@inject('counterStore' ) @observer class App extends React .Component { render() { const { counterStore, title } = this .props; return ( <div> <div>{title}</div> <button onClick={() => counterStore.increment()}>+1 </button> <span>{counterStore.counter}</span> <button onClick={() => counterStore.decrement()}>-1 </button> </div> ); } } export default App;
Converting to TypeScript Important:
This will only work with Mobx 5 as v6 dropped decorators support. Discussion
You will also need to stay on mobx-react
v6 to be compatible with mobx 5 .
If you are starting from scratch and you can get away with only injecting your stores through hooks, you don’t need all this.
1. Filenames Convert all the files to .ts
and .tsx
2. Convert the store to TS Pretty straightforward, add the type for counter
:
[...] @observable counter: number = 0 ; [...]
3. Typed Props for the App Component Let’s split the injected stores props and the actual props (you will see why later).
import { CounterStore } from './counterStore' ;type StoreProps = { counterStore: CounterStore; }; interface Props extends StoreProps { title: string; } @inject('counterStore' ) @observer class App extends React .Component <Props > { [...] }; export default App;
The warnings in our App
component are now gone but the index.ts
file is complaining about the missing counterStore
.
4. Fixing the App Component exported type First of all we need to specify defaultProps
so that the compiler does not expect us to pass the store down
@inject('counterStore' ) @observer class App extends React .Component <Props > { static defaultProps = {} as StoreProps; render() { [...]
then we need to adjust the type of the default export (our Component). This will help TypeScript recognize the fact that App
has a static wrappedComponent
property.
This is especially useful in unit tests as you will probably want to test App.wrappedComponent
instead of App
.
Here is how to export the correct type:
export default App as (typeof App & IWrappedComponent<Props>);
Your app is now ready to go !
5. Full code import React from 'react' ;import { render } from 'react-dom' ;import { Provider } from 'mobx-react' ;import store from './counterStore' ;import App from './App' ;const rootElement = document .getElementById('root' );render( <Provider counterStore={store}> <App title='normal prop title' /> </Provider>, rootElement ); import { observable } from 'mobx' ;export class CounterStore { @observable counter: number = 0 ; increment() { this .counter++; } decrement() { this .counter--; } } export default new CounterStore();import React from 'react' ;import { inject, IWrappedComponent, observer } from 'mobx-react' ;import { CounterStore } from './counterStore' ;type StoreProps = { counterStore: CounterStore; }; interface Props extends StoreProps { title: string; } @inject('counterStore' ) @observer class App extends React .Component <Props > { static defaultProps = {} as StoreProps; render() { const { counterStore, title } = this .props; return ( <div> <div>{title}</div> <button onClick={() => counterStore.increment()}>+1 </button> <span>{counterStore.counter}</span> <button onClick={() => counterStore.decrement()}>-1 </button> </div> ); } } export default App as typeof App & IWrappedComponent<Props>;