A few years ago, I worked on an Elm app at thoughtbot and it completely changed how I think about building software. I’ve been chasing that feeling ever since. Unfortunately, it’s been hard to convince managers to adopt Elm, despite its myriad benefits.
I mostly work with TypeScript and Scala professionally. Lots of React the last several years. I have spent a lot of time bringing Elm-ish patterns into my React work. I realized about a year ago that Effect makes “The Elm Architecture in TypeScript” genuinely viable, so I decided to build it.
The result is Foldkit (website) (repo).
Here’s a complete counter application in Foldkit.
import { Match as M, Schema as S } from 'effect'
import { Runtime } from 'foldkit'
import { Command } from 'foldkit/command'
import { Html, html } from 'foldkit/html'
import { m } from 'foldkit/message'
// MODEL
const Model = S.Struct({ count: S.Number })
type Model = typeof Model.Type
// MESSAGE
const ClickedDecrement = m('ClickedDecrement')
const ClickedIncrement = m('ClickedIncrement')
const ClickedReset = m('ClickedReset')
const Message = S.Union(ClickedDecrement, ClickedIncrement, ClickedReset)
type Message = typeof Message.Type
// UPDATE
const update = (
model: Model,
message: Message,
): [Model, ReadonlyArray<Command<Message>>] =>
M.value(message).pipe(
M.withReturnType<[Model, ReadonlyArray<Command<Message>>]>(),
M.tagsExhaustive({
ClickedDecrement: () => [{ count: model.count - 1 }, []],
ClickedIncrement: () => [{ count: model.count + 1 }, []],
ClickedReset: () => [{ count: 0 }, []],
}),
)
// INIT
const init: Runtime.ElementInit<Model, Message> = () => [{ count: 0 }, []]
// VIEW
const { div, button, Class, OnClick } = html<Message>()
const view = (model: Model): Html =>
div(
[
Class(
'min-h-screen bg-white flex flex-col items-center justify-center gap-6 p-6',
),
],
[
div(
[Class('text-6xl font-bold text-gray-800')],
[model.count.toString()],
),
div(
[Class('flex flex-wrap justify-center gap-4')],
[
button([OnClick(ClickedDecrement()), Class(buttonStyle)], ['-']),
button([OnClick(ClickedReset()), Class(buttonStyle)], ['Reset']),
button([OnClick(ClickedIncrement()), Class(buttonStyle)], ['+']),
],
),
],
)
// STYLE
const buttonStyle = 'bg-black text-white hover:bg-gray-700 px-4 py-2 transition'
// RUN
const element = Runtime.makeElement({
Model,
init,
update,
view,
container: document.getElementById('root')!,
})
Runtime.run(element)
A few other things that Elm developers will find familiar:
- Commands — side effects are described as Effects that return Messages. The runtime executes them.
- Subscriptions — declared as a function of the Model. The runtime diffs and manages them.
- Bidirectional routing — URLs parse into typed routes and routes build back into URLs.
- Submodels + OutMessage — child modules communicate up through an explicit message pattern.
- DevTools with time travel — built-in overlay for inspecting Messages and Model state, with the ability to jump to any point in history.
What’s different from Elm:
- It’s TypeScript, so you’re in the npm ecosystem with all that implies.
- Effect gives you a real effect system, but it’s a library, not a language feature — the compiler won’t stop you from doing side effects in your update function. The architecture is enforced by convention and code review, not by the compiler. This is the biggest tradeoff.
- Views use a virtual DOM (Snabbdom) with an HTML builder API rather than a template language.
Curious what people here think. In the nine months since starting Foldkit, I’ve shared progress updates in the Effect Discord, but have never shared here because, while Foldkit is basically a love letter to Elm, it’s not Elm itself.
Thank you!