Type‑Safe Click Handling in Elm: Leveraging onClick with Exhaustive Update
Learn how Elm’s Html.Events gives you compile‑time safe click handlers, exhaustive update checking, and how to avoid namespace clashes.
08 Sept 2025, 22:45 UTC

Problem: unsafe click handlers in JavaScript
In plain JavaScript attaching a click listener often means writing a callback that can throw, receive unexpected event properties, or forget to preventDefault. Errors surface only at runtime, making refactors risky.
Thesis: Elm’s Html.Events gives you typed, exhaustively checked clicks
By describing the view with Elm’s HTML DSL, each onClick is paired with a Json.Decode decoder that turns the browser event into a custom Msg. The compiler then guarantees that every possible Msg is handled in the update function.
Section 1: From Html to a Message
When you write
button [ onClick (Decode.succeed Increment) ] [ text "+" ]
Elm translates the attribute into a virtual‑DOM node that registers a listener. The listener calls the pure function Decode.succeed Increment which returns a Msg. No runtime type checks are needed because the decoder is fixed at compile time.
Section 2: Typed input with onInput
For a text field you can decode the value:
input [ placeholder "Enter number"
, onInput (Decode.map SetNumber targetValue) ] []
Here targetValue : Json.Decode String extracts event.target.value. The resulting SetNumber String message carries a known string, eliminating the need to guard against missing properties.
Section 3: Exhaustive update
The update function might look like:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Increment ->
( { model | count = model.count + 1 }, Cmd.none )
SetNumber str ->
( { model | input = str }, Cmd.none )
If you add a new constructor to Msg (e.g. Reset) and forget to add a branch, Elm emits:
-- MISSING PATTERNS -------------------------------------------------
This `case` does not have branches for the following patterns:
Reset
Thus the compiler forces you to handle every message, preventing unhandled‑click bugs.
Trade‑off: namespace collisions
In a large project you might have a local module named Events.elm. Importing Html.Events then exposes onClick and onInput under the same name as your own helpers, leading to ambiguous references. The remedy is to qualify the import:
import Html.Events exposing (onClick, onInput)
-- or
import Html.Events as HE
Qualified access (HE.onClick) avoids clashes while keeping the benefits of type‑safe event handling.
Actionable steps
- Write your view using
Html.Events.onClickoronInputwith a decoder. - Define a
Msgtype that captures all user interactions. - Implement
updatewith acaseonMsg; let the compiler warn about missing branches. - Run
elm make src/Main.elmand verify zero warnings. - Optionally inspect the generated JavaScript (
elm make --output=main.js) to see that listeners call plain functions withouttypeofchecks. - Add a test with
elm-testthat clicks the button and asserts the resulting model.
By following these steps you get click handling that is as safe as the rest of your Elm code: no runtime surprises, full exhaustiveness, and a clear path to refactor.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.