Skip to main content

TypeScript

Rematch handles TypeScript inference practically out of the box, we have all our codebase with TypeScript (latest version) and we do continuous testing to our TypeScript examples.

For getting a cool TypeScript setup with Rematch, it's as easy as using createModel helper.

createModel

Use helper method createModel to create a model.

ts
import { createModel } from '@rematch/core'
import type { RootModel } from './models'
 
export const count = createModel<RootModel>()({
state: 0,
reducers: {
increment(state, payload: number) {
return state + payload
},
},
effects: (dispatch) => ({
incrementAsync(payload: number, state) {
dispatch.count.increment(payload)
},
}),
})
ts
import { createModel } from '@rematch/core'
import type { RootModel } from './models'
 
export const count = createModel<RootModel>()({
state: 0,
reducers: {
increment(state, payload: number) {
return state + payload
},
},
effects: (dispatch) => ({
incrementAsync(payload: number, state) {
dispatch.count.increment(payload)
},
}),
})

In the case of a complex state, you can just type the state with the as keyword:

ts
import { createModel } from '@rematch/core'
import type { RootModel } from './models'
 
type Names = 'custom'
type ComplexCountState = {
count: number
multiplierName: Names
}
 
export const count = createModel<RootModel>()({
state: {
count: 0,
multiplierName: 'custom',
} as ComplexCountState,
reducers: {
increment(state, payload: number) {
return {
count: state.count + payload,
multiplierName: 'custom',
}
},
},
effects: (dispatch) => ({
incrementEffect(payload: number, rootState) {
dispatch.count.increment(payload)
},
}),
})
ts
import { createModel } from '@rematch/core'
import type { RootModel } from './models'
 
type Names = 'custom'
type ComplexCountState = {
count: number
multiplierName: Names
}
 
export const count = createModel<RootModel>()({
state: {
count: 0,
multiplierName: 'custom',
} as ComplexCountState,
reducers: {
increment(state, payload: number) {
return {
count: state.count + payload,
multiplierName: 'custom',
}
},
},
effects: (dispatch) => ({
incrementEffect(payload: number, rootState) {
dispatch.count.increment(payload)
},
}),
})

RootModel

RootModel is the file that stores all your models. We need it because you can dispatch effects and access state from other models (it's global), so we need to know all the models for bringing you the intellisense.

models.ts
ts
// @filename: models.ts
import { Models } from "@rematch/core"
import { count } from "./count"
 
export interface RootModel extends Models<RootModel> {
count: typeof count
}
 
export const models: RootModel = { count }
models.ts
ts
// @filename: models.ts
import { Models } from "@rematch/core"
import { count } from "./count"
 
export interface RootModel extends Models<RootModel> {
count: typeof count
}
 
export const models: RootModel = { count }

init() store

init

With your model ready with createModel() helper and the RootModel exported, you only need to init() the store.

Now we like to export some common types:

  • Store: type
  • RematchDispatch: useful for knowing all the effects and reducers methods and his parameters
  • RematchRootState: you will get intellisense of each state of each model.
store.ts
ts
import { init, RematchDispatch, RematchRootState } from '@rematch/core'
import { models, RootModel } from './models'
 
export const store = init({
models,
})
 
export type Store = typeof store
export type Dispatch = RematchDispatch<RootModel>
export type RootState = RematchRootState<RootModel>
store.ts
ts
import { init, RematchDispatch, RematchRootState } from '@rematch/core'
import { models, RootModel } from './models'
 
export const store = init({
models,
})
 
export type Store = typeof store
export type Dispatch = RematchDispatch<RootModel>
export type RootState = RematchRootState<RootModel>
tip

In the case you use some plugin, please read this:

init with plugins

Some plugins modifies the store like @rematch/loading, that introduces a new state with all your promises status, TypeScript to know that needs some helper.

You need to pass the RootModel to init() function and introduce the helpers:

  • @rematch/loading: { ExtraModelsFromLoading }
  • @rematch/updated: { ExtraModelsFromUpdated }
store.ts
ts
// @filename: store.ts
store.ts
ts
// @filename: store.ts

React Hooks Types

  • RootState and Dispatch types: import this types from the previous file mentioned: init

useSelector

tsx
import React from 'react'
import { RootState } from './store'
import { useSelector } from 'react-redux'
 
const Count = () => {
const countState = useSelector((state: RootState) => state.count)
 
return <div>example</div>
}
tsx
import React from 'react'
import { RootState } from './store'
import { useSelector } from 'react-redux'
 
const Count = () => {
const countState = useSelector((state: RootState) => state.count)
 
return <div>example</div>
}

useDispatch

tsx
import React, { useEffect } from 'react'
import { Dispatch } from './store'
import { useDispatch } from 'react-redux'
 
const Count = () => {
const dispatch = useDispatch<Dispatch>()
useEffect(() => {
dispatch.count.incrementAsync(2)
(property) incrementAsync: (payload: number) => void
}, [])
 
return <div>example</div>
}
tsx
import React, { useEffect } from 'react'
import { Dispatch } from './store'
import { useDispatch } from 'react-redux'
 
const Count = () => {
const dispatch = useDispatch<Dispatch>()
useEffect(() => {
dispatch.count.incrementAsync(2)
(property) incrementAsync: (payload: number) => void
}, [])
 
return <div>example</div>
}

React class types

tsx
import React from 'react'
import { RootState, Dispatch } from './store'
import { connect } from 'react-redux'
 
class App extends React.PureComponent<Props> {
render() {
const { countState } = this.props
return <div>example</div>
}
}
 
const mapState = (state: RootState) => ({
countState: state.count,
})
 
const mapDispatch = (dispatch: Dispatch) => ({
count: dispatch.count,
})
 
type StateProps = ReturnType<typeof mapState>
type DispatchProps = ReturnType<typeof mapDispatch>
type Props = StateProps & DispatchProps
 
export default connect(mapState, mapDispatch)(App)
tsx
import React from 'react'
import { RootState, Dispatch } from './store'
import { connect } from 'react-redux'
 
class App extends React.PureComponent<Props> {
render() {
const { countState } = this.props
return <div>example</div>
}
}
 
const mapState = (state: RootState) => ({
countState: state.count,
})
 
const mapDispatch = (dispatch: Dispatch) => ({
count: dispatch.count,
})
 
type StateProps = ReturnType<typeof mapState>
type DispatchProps = ReturnType<typeof mapDispatch>
type Props = StateProps & DispatchProps
 
export default connect(mapState, mapDispatch)(App)

Effects returning values

There's a situation where if you're accessing the rootState value of the same model and returning this value. TypeScript will fail because circular references itself (has sense)...

You should try to avoid returning values on effects and just dispatch data to reducers or write pure functions outside Rematch for a better performance.

Anyways, you can omit this error force typing the effect. Related Github Issue

Instead of:

ts
import { createModel } from '@rematch/core'
import { RootModel } from './models'
 
export const count = createModel<RootModel>()({
Type of property 'count' circularly references itself in mapped type '{ [x: string]: any; count: any; }'.2615Type of property 'count' circularly references itself in mapped type '{ [x: string]: any; count: any; }'.
state: 0,
reducers: {
increment(state, payload: number) {
return state + payload
},
},
effects: (dispatch) => ({
async incrementAsync(payload: number, state) {
dispatch.count.increment(payload)
return state.count
},
}),
})
ts
import { createModel } from '@rematch/core'
import { RootModel } from './models'
 
export const count = createModel<RootModel>()({
Type of property 'count' circularly references itself in mapped type '{ [x: string]: any; count: any; }'.2615Type of property 'count' circularly references itself in mapped type '{ [x: string]: any; count: any; }'.
state: 0,
reducers: {
increment(state, payload: number) {
return state + payload
},
},
effects: (dispatch) => ({
async incrementAsync(payload: number, state) {
dispatch.count.increment(payload)
return state.count
},
}),
})

Define the return value:

ts
import { createModel } from '@rematch/core'
import { RootModel } from './models'
 
export const count = createModel<RootModel>()({
state: 0,
reducers: {
increment(state, payload: number) {
return state + payload
},
},
effects: (dispatch) => ({
async incrementAsync(payload: number, state): Promise<number> {
dispatch.count.increment(payload)
return state.count
},
}),
})
ts
import { createModel } from '@rematch/core'
import { RootModel } from './models'
 
export const count = createModel<RootModel>()({
state: 0,
reducers: {
increment(state, payload: number) {
return state + payload
},
},
effects: (dispatch) => ({
async incrementAsync(payload: number, state): Promise<number> {
dispatch.count.increment(payload)
return state.count
},
}),
})