Overmind is written in Typescript and it is written with a focus on you dedicating as little time as possible to help Typescript understand what your app is all about. Typescript will spend a lot more time helping you. If you are not a Typescript developer Overmind is a really great project to start learning it as you will get the most out of the little typing you have to do.
Configuration
First we need to define the typing of our configuration and there are two approaches to that.
1. Declare module
The most straightforward way to type your application is to use the declare module approach. This will work for most applications, but might make you feel uncomfortable as a hardcore Typescripter. The reason is that we are overriding an internal type, meaning that you can only have one instance of Overmind running inside your application.
import { IConfig } from'overmind'constconfig= {}declaremodule'overmind' {// eslint-disable-next-line @typescript-eslint/no-empty-interfaceinterfaceConfigextendsIConfig<{ state:typeofconfig.state, actions:typeofconfig.actions, effects:typeofconfig.effects }> {}// Due to circular typing we have to define an// explicit typing of state, actions and effects since// TS 3.9}
Now you can import any type directly from Overmind and it will understand the configuration of your application. Even the operators are typed.
import { Context, RootState, pipe, map, filter, ...// These are primitive types in Overmind you typically// do not need Action, AsyncAction, Operator} from'overmind'
2. Explicit typing
You can also explicitly type your application. This gives more flexibility.
import { IConfig, IOnInitialize, IContext,} from'overmind'exportconstconfig= {}// Due to circular typing we have to define an// explicit typing of state, actions and effects since// TS 3.9exportinterfaceConfigextendsIConfig<{ state:typeofconfig.state, actions:typeofconfig.actions, effects:typeofconfig.effects}> {}exportinterfaceOnInitializeextendsIOnInitialize<Config> {}exportinterfaceContextextendsIContext<Config> {}exporttypeRootState=Context['state']/* NOTE! These types are typically not needed, but represents primitives in Overmind which you might want to pass as arguments etc.*/exportinterfaceAction<Input=void,Output=void> extendsIAction<Config,Input,Output> {}exportinterfaceAsyncAction<Input=void,Output=void> extendsIAction<Config,Input,Promise<Output>> {}exportinterfaceOperator<Input=void,Output=Input> extendsIOperator<Config,Input,Output> {}exportinterfaceOperator<Input=void,Output=Input> extendsIOperator<Config,Input,Output> {}
You only have to set up these types once, where you bring your configuration together. That means if you use multiple namespaced configuration you still only create one set of types, as shown above.
Now you only have to make sure that you import your types from this file, instead of directly from the Overmind package.
The Overmind documentation is written for implicit typing. That means whenever you see a type import directly from the Overmind package, you should rather import from your own defined types.
State
The state you define in Overmind is just an object where you type that object.
It is important that you use a type and not an interface. This has to do with the way Overmind resolves the state typing.
When writing Typescript you should not use optional values for your state (?), or use undefined in a union type. In a serializable state store world null is the value indicating “there is no value”.
typeState= {// Do not do this foo?:string// Do not do this foo:string|undefined// Do this foo:string|null// Or this, if there always will be a value there foo:string}exportconststate:State= { foo:null}
Operators is like the action: it can take an optional value, but it always produces an output. By default the output of an operator is the same as the input.
import { Context, mutate, filter, map } from'overmind'// Use the Context type for the first argumentexportconstchangeSomeState=mutate(({ state }:Context) => {state.foo ='bar'})// Type the value as the second argumentexportconstfilterAwesomeUser=filter((_:Context, user:User) => {returnuser.isAwesome})// The output is inferredexportconsttoNumber=map((_:Context, value:number) => { returnNumber(value)})
When you create a pipe that has an input when it is called you only need to type the first operator value.
import { Context, pipe, map, mutate } from'overmind'exportconstdoThis=pipe(map((context:Context, value:string) => {// actions.doThis("foo"), requires "string"return123 }),mutate((context:Context, value) => {// value is now "number" })