Typescript

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

The only typing you need is the Context. This holds information about your state, actions and effects.

import { IContext } from 'overmind'

export const config = {}

export type Context = IContext<typeof config>

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 a single Context type.

State

The state you define in Overmind is just an object where you type that object.

type State = {
  foo: string
  bar: boolean
  baz: string[]
  user: User
}

export const state: State = {
  foo: 'bar',
  bar: true,
  baz: [],
  user: new User()
}

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”.

type State = {
  // 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
}

export const state: State = {
  foo: null
}

Derived

import { derived } from 'overmind'

type State = {
  foo: string
  shoutedFoo: string
}

export const state: State = {
  foo: 'bar',
  shoutedFoo: derived((state: State) => state.foo + '!!!')
}

Note that the type argument you pass is the object the derived is attached to, so with nested derived:

import { derived } from 'overmind'

type State = {
  foo: string
  nested: {
    shoutedFoo: string
  }
}

export const state: State = {
  foo: 'bar',
  nested: {
    shoutedFoo: derived((state: State['nested']) => state.foo + '!!!')
  }
}

To access the root state you can use your Context type:

import { Context } from 'app/overmind'

type State = {
  foo: string
  shoutedFoo: string
}

export const state: State = {
  foo: 'bar',
  shoutedFoo: derived(
    (state: State, rootState: Context["state"]) => state.foo + '!!!'
  )
}

Statemachine

Read the guide on Using state machines to understand how to type them.

Actions

You type your actions with the Context and an optional value. Any return type will be inferred.

import { Overmind } from 'overmind'
import { Context } from 'app/overmind'

export const noArgAction = (context: Context) => {
  // actions.noArgAction()
}

export const argAction = (context: Context, value: string) => {
  // actions.argAction("foo"), requires "string"
}

export const noArgWithReturnTypeAction = (context: Context) => {
  // actions.noArgWithReturnTypeAction(), with return type "string"
  return 'foo'
}

export const argWithReturnTypeAction = (context: Context, value: string) => {
  // actions.argWithReturnTypeAction("foo"), requires "string" and returns "string"
  return value + '!!!'
}

// The onInitialize action
export const onInitializeOvermind = (context: Context, instance: Overmind<Context>) => {

}

Any of these actions could be defined as an async function or simply return a promise to be typed that way.

Effects

There are no Overmind specific types related to effects, you just type them in general.

export const api = {
  getUser: async (): Promise<User> => {
    const response = await fetch('/user')
    
    return response.json()
  }
}

Operators

Operators is like the action: it can take an optional value, but it always produces a promise output. By default the promised value of an operator is the same as the input.

import { Context, filter } from 'overmind'

// Actions are interoperable with operators. So type them
// like an action
export const changeSomeState = ({ state }: Context) =>  {
  state.foo = 'bar'
}

// Most operators takes an action signature, just type it as that
export const filterAwesomeUser = filter((_: Context, user: User) => 
  user.isAwesome
})

When you create a pipe and inline other operators/actions their payloads are inferred. Only the first operator needs to type its payload so that when calling doThis you will have the correct typing for the initial payload.

import { Context, pipe } from 'overmind'

export const doThis = pipe(
  (context: Context, value: string) => {
    // actions.doThis("foo"), requires "string"
    return 123
  },
  (context: Context, value) => {
    // value is now "number"
  }
)

// call action
doThis('foo') // Typed to string, as first operator needs string

Last updated