TypeScript

Starting with version 2.2, Kea officially supports TypeScript!

In addition to increased type safety, this massively improves developer ergonomics, as you can now autocomplete all your actions and values, both while using logic in a component:

Kea TypeScript React Component

... and while writing it:

Kea TypeScript in Logic

There's just one gotcha.

TypeScript doesn't support the funky loopy syntax that Kea uses, making impossible to automatically generate types for code like this:

// The TS Compiler doesn't like code like this, where all input is
// in one big object that depends on different parts of itself and
// also on the produced output (`logic`)
const logic = kea({
actions: {
openBlog: (id: number) => ({ id }),
closeBlog: true,
},
reducers: {
blog: [
null,
{
// We're still inside the same huge object that
// defines these actions just a few lines above
openBlog: (_, { id }) => id,
closeBlog: () => null,
// How to autocomplete the list of valid actions here?
// How to get `{ id: number }` for `openBlog`?
},
],
},
// Listeners are defined as a function that gets the `logic`
// as its parameter, but we're still building it, so 🤷
listeners: ({ actions }) => ({
openBlog: async ({ id }, breakpoint) => {
await breakpoint(10000)
// How can we get this to work?
actions.closeBlog()
},
}),
})
// Only here, after the logic is fully defined, we could have
// automatic autocompletion, but that's not enough.
function Blog () {
const { openBlog, ... } = useActions(logic)
return <div />
}

I tried many ways to get this to work, but it was just not possible. Even if I'd totally change the syntax of Kea itself, several things would still not be possible with today's TypeScript. For example typing selectors that recursively depend on each other... or supporting plugins.

Check out this blog post for a full overview of all the failed approaches.

Thus a workaround was needed.

Option 1: kea-typegen

The best way to get types in your logic is with kea-typegen, which uses the TypeScript Compiler API to analyse your .ts files and generate type definitions for your logic.

Running kea-typegen write or kea-typegen watch will generate a bunch of logicType.ts files:

Kea-TypeGen

... which you must then import and pass on to the kea() call.

import { kea } from 'kea'
import { githubLogicType } from './githubLogicType'
export const githubLogic = kea<githubLogicType>({
// ^^^^^^^^^^^^^^^^^ 👈
actions: { ... },
reducers: { ... },
listeners: ({ actions }) => ({ ... })
})

It's a bit of extra work, but works like magic once set up!

If, like in the screencast above, you have logic that connects to other logic or selectors that depend on other selectors kea-typegen will run in multiple passes until there are no more changes to write.

Setup

First install the kea-typegen and typescript packages:

# if you're using yarn
yarn add --dev kea-typegen typescript
# if you're using npm
npm install kea-typegen typescript --save-dev

Then create a .kearc file in the root of your project, with the relevant paths:

{
"tsConfigPath": "./tsconfig.json",
"rootPath": "./src",
"typesPath": "./types"
}

You may also specify these paths as arguments to the kea-typegen commands, though it's easier to have them in the .kearc file.

Usage

  • While developing, run kea-typegen watch, and it'll generate new types every time your logic changes.
  • Run kea-typegen write to generate all the types, for example before a production build.
  • Finally, kea-typegen check can be used to see if any types need to be written.

I recommend keeping all the generated types in an adjacent folder to your app's sources and adding it to .gitignore.

You can, of course, save the logicType.ts files next to the logic.ts files themselves and commit them in, but in my experience doing so causes a lot of headache, especially when working with many branches and pull requests.

Here's a sample pacakge.json, that uses concurrently to run kea-typegen watch together with webpack while developing and kea-typegen write before building the production bundle.

// package.json
{
"scripts": {
"start": "concurrently start:webpack start:typegen -n WEBPACK,TYPEGEN -c blue,green",
"start:typegen": "kea-typegen watch",
"start:webpack": "webpack-dev-server",
"build": "yarn run build:typegen && yarn run build:webpack",
"build:typegen": "kea-typegen write",
"build:webpack": "NODE_ENV=production webpack --config webpack.config.js"
}
}

Types in Reducers

kea-typegen automatically detects the types used in your actions, reducers, selectors and so on. It may however be the case that you need to manually specify the type of your reducers.

In the following example the type of blogId is be autodetected as null, since we can't read more out of the default value.

Using the as keyword you can improve on this and provide the exact type for your reducer:

const logic = kea({
actions: {
openBlog: (id: number) => ({ id }),
closeBlog: true,
},
reducers: {
blogId: [
null as number | null, // 👈 it can also be a number
{
openBlog: (_, { id }) => id,
closeBlog: () => null,
},
],
},
})

Rough Edges

This is the very first version of kea-typegen, so there are still some rough edges.

  1. You must manually import the logicType and insert it into your logic. This will be done automatically in the future.
Import Logic Type Manually
  1. You must manually hook up all type dependencies by adding them on the logicType in logic.ts. kea-typegen will then put the same list inside logicType. This will also be done automatically in the future.
Send Type to Logic Type
  1. When connecting logic together, you must use [otherLogic.actionTypes.doSomething] instead of [otherLogic.actions.doSomething]
Use ActionTypes
  1. Sometimes you might need to "Reload All Files" in your editor... or explicitly open logicType.ts to see the changes.

  2. Plugins aren't supported yet. I've hardcoded a few of them (loaders, router, window-values) into the typegen library, yet that's not a long term solution.

  3. logic.extend() doesn't work yet.

These are all solvable issues. Let me know which ones to prioritise!

Option 2: MakeLogicType<V, A, P>

At the end of the day, we are forced to make our own logicTypes and feed them to kea() calls.

However nothing says these types need to be explicitly made by kea-typegen. You could easily make them by hand. Follow the example and adapt as needed!

To help with the most common cases, Kea 2.2.0 comes with a special type:

import { MakeLogicType } from 'kea'
type MyLogicType = MakeLogicType<Values, Actions, Props>

Pass it a bunch of interfaces denoting your logic's values, actions and props... and you'll get a close-enough approximation of the generated logic.

interface Values {
id: number
created_at: string
name: string
pinned: boolean
}
interface Actions {
setName: (name: string) => { name: string }
}
interface Props {
id: number
}
type RandomLogicType = MakeLogicType<Values, Actions, Props>
const randomLogic = kea<RandomLogicType>({
/* skipping for brevity */
})

The result is a fully typed experience:

MakeLogicType

You'll even get completion when coding the logic:

MakeLogicType Reducers

Thank you to the team at Elastic for inspiring this approach!

Next steps
  • Read about Debugging to be even more productive when writing Kea code.