Elm Cheat Sheet
Core syntax, the Elm Architecture (Model/Update/View), types, and JSON decoding for building reliable, no-runtime-exceptions front-end apps.
Core Syntax & Types
Values, functions, type annotations, and records.
-- Values are immutable; type annotations are optional but idiomaticgreeting : Stringgreeting = "Hello, Elm!"square : Int -> Intsquare n = n * n-- Recordstype alias User = { name : String , age : Int }alice : Useralice = { name = "Alice", age = 30 }-- Update a record (returns a new one)older : Userolder = { alice | age = alice.age + 1 }-- Custom types (ADTs)type Status = Loading | Success String | Failure Stringdescribe : Status -> Stringdescribe status = case status of Loading -> "loading..." Success msg -> "ok: " ++ msg Failure err -> "error: " ++ err
The Elm Architecture
The Model/Update/View pattern every Elm app is built on.
module Main exposing (main)import Browserimport Html exposing (Html, button, div, text)import Html.Events exposing (onClick)type alias Model = Intinit : Modelinit = 0type Msg = Increment | Decrementupdate : Msg -> Model -> Modelupdate msg model = case msg of Increment -> model + 1 Decrement -> model - 1view : Model -> Html Msgview model = div [] [ button [ onClick Decrement ] [ text "-" ] , div [] [ text (String.fromInt model) ] , button [ onClick Increment ] [ text "+" ] ]main : Program () Model Msgmain = Browser.sandbox { init = init, update = update, view = view }
JSON Decoding
Elm has no null/undefined, so incoming JSON must be decoded explicitly.
import Json.Decode as Decode exposing (Decoder, field, string, int, list)type alias Post = { id : Int , title : String }postDecoder : Decoder PostpostDecoder = Decode.map2 Post (field "id" int) (field "title" string)postsDecoder : Decoder (List Post)postsDecoder = list postDecoder-- decodeString postDecoder "{\"id\":1,\"title\":\"Hi\"}"-- => Ok { id = 1, title = "Hi" }
Commands & Http
Side effects (HTTP, ports, random) go through Cmd, not directly in update.
import Httptype Msg = GotPosts (Result Http.Error (List Post))fetchPosts : Cmd MsgfetchPosts = Http.get { url = "/api/posts" , expect = Http.expectJson GotPosts postsDecoder }update : Msg -> Model -> ( Model, Cmd Msg )update msg model = case msg of GotPosts (Ok posts) -> ( { model | posts = posts }, Cmd.none ) GotPosts (Err _) -> ( model, Cmd.none )
elm CLI & Tooling
Common commands from the elm binary and ecosystem.
- elm init- scaffold a new project (creates elm.json)
- elm make src/Main.elm --output=main.js- compile to JS
- elm make src/Main.elm --optimize- production build, smaller output
- elm reactor- dev server with live compile at localhost:8000
- elm repl- interactive shell for trying expressions
- elm install author/package- add a dependency to elm.json
- elm-test- run the elm-explorations/test suite
- elm-format- canonical formatter, run on save
Modeling Remote Data Without Booleans
Replace isLoading/error/data boolean soup with a single custom type so the compiler forces exhaustive handling.
type RemoteData e a = NotAsked | Loading | Failure e | Success atype alias Model = { posts : RemoteData Http.Error (List Post) }view : Model -> Html Msgview model = case model.posts of NotAsked -> text "" Loading -> text "Loading..." Failure err -> text ("Error: " ++ Debug.toString err) Success posts -> ul [] (List.map viewPost posts)-- Impossible to accidentally render a spinner AND an error at once ---- the type only allows one state at a time.
Ports: Talking to JavaScript
Ports are the only sanctioned escape hatch for interop -- typed message passing, no direct JS calls from Elm.
port module Main exposing (main)-- Outgoing: Elm -> JSport saveToLocalStorage : String -> Cmd msg-- Incoming: JS -> Elmport onStorageChange : (String -> msg) -> Sub msgtype Msg = Save String | ExternalChange Stringupdate : Msg -> Model -> ( Model, Cmd Msg )update msg model = case msg of Save value -> ( model, saveToLocalStorage value ) ExternalChange value -> ( { model | value = value }, Cmd.none )subscriptions : Model -> Sub Msgsubscriptions _ = onStorageChange ExternalChange-- JS side:-- app.ports.saveToLocalStorage.subscribe(v => localStorage.setItem('k', v))-- app.ports.onStorageChange.send(localStorage.getItem('k'))
Task: Chaining Effects Explicitly
Task lets you sequence and combine effects (unlike Cmd, which is fire-and-forget) before turning the result into a Msg.
import Task exposing (Task)import TimegetPostThenAuthor : Int -> Task Http.Error ( Post, Author )getPostThenAuthor postId = getPostTask postId |> Task.andThen (\post -> getAuthorTask post.authorId |> Task.map (\author -> ( post, author )) )-- Run several tasks concurrently and combine resultsTask.map2 Tuple.pair Time.now Time.here |> Task.perform GotTimeInfo-- Convert a Task into a Cmd (Task never fails silently -- Err must be handled)ToTask : Cmd MsgToTask = Task.attempt HandleResult (getPostThenAuthor 1)
Custom & Fallback Decoders
Handle inconsistent JSON shapes (unions, optional fields, string-to-type coercion) with decoder combinators.
import Json.Decode as Decode exposing (Decoder)-- Decode a union type from a string tag fieldstatusDecoder : Decoder StatusstatusDecoder = Decode.field "status" Decode.string |> Decode.andThen (\s -> case s of "loading" -> Decode.succeed Loading "success" -> Decode.map Success (Decode.field "message" Decode.string) _ -> Decode.fail ("Unknown status: " ++ s) )-- Optional field with a defaultageDecoder : Decoder IntageDecoder = Decode.oneOf [ Decode.field "age" Decode.int , Decode.succeed 0 ]-- Try multiple shapes in orderidDecoder : Decoder StringidDecoder = Decode.oneOf [ Decode.string , Decode.map String.fromInt Decode.int ]
Advanced Tooling & Ecosystem
Beyond the core CLI: testing, performance, and interop tools used in production Elm apps.
- elm-explorations/test fuzz tests- property-based testing with `Fuzz.int`, `Fuzz.list`, etc. generates random inputs to catch edge cases
- elm-review- static analysis tool that enforces custom rules (unused imports, forbidden patterns) at CI time
- Debug.log / Debug.toString- compiler refuses to build with `Debug.*` calls present, preventing accidental shipping of debug code
- elm/json Decode.Pipeline (NoRedInk)- `|> required "name" string |> optional "age" int 0` style for decoding wide records ergonomically
- Browser.application- full SPA with URL routing via `onUrlChange`/`onUrlRequest`, distinct from `Browser.sandbox`/`Browser.element`
- elm-optimize-level-2- community optimizer that applies extra JS-level transforms beyond `--optimize` for smaller/faster bundles
- Html.Lazy- `Html.Lazy.lazy view model` skips re-rendering a subtree when its arguments are referentially unchanged
Model illegal states out of existence with custom types (e.g. RemoteData Error (List Post) instead of separate isLoading/error/data fields) — the compiler will then force you to handle every case, eliminating whole classes of runtime bugs.