A condensed reference of common Elm patterns. Each entry gives you the core idea and code — enough for a coding agent or experienced Elm developer to apply the pattern without reading a full article. Grouped by concept rather than difficulty.

Type Safety

Patterns that use Elm’s type system to catch bugs at compile time.

Type blindness

Wrap primitive values in custom types to prevent mixing them up (e.g. dollars and euros).

-- ❌ Both are Float, nothing stops dollars + euros
priceInDollars : Float
priceInEuros : Float

-- ✓ Compiler distinguishes them
type Dollar = Dollar Float
type Euro = Euro Float

Minimize boolean usage

Replace booleans with custom types to make intent explicit and let the compiler enforce correctness.

-- ❌ What does True mean here?
bookFlight "ELM" True

-- ✓ Intent is self-documenting
type CustomerStatus = Premium | Regular | Economy
bookFlight "ELM" Premium

Return custom types instead of Bool to eliminate boolean blindness:

-- ❌ isValid returns Bool, nothing stops using invalid data
if isValid formData then submitForm formData else ...

-- ✓ Compiler enforces valid data at each step
case validate formData of
    Ok valid -> submitForm valid
    Err errors -> showErrors errors

Named arguments

Use a record argument when function parameter order is ambiguous.

-- ❌ Which date is the subject?
isBefore : Date -> Date -> Bool

-- ✓ Self-documenting
isBefore : { subject : Date, comparedTo : Date } -> Bool

Wrap early, unwrap late

Wrap primitive values in custom types at the boundary (decoding) and unwrap as late as possible.

-- ❌ Any Float can be passed
displayPrice : Float -> String

-- ✓ Only Dollars accepted
type Dollar = Dollar Float
displayPrice : Dollar -> String
displayPrice (Dollar price) = "USD$" ++ String.fromFloat price

Make impossible states impossible

Replace boolean + maybe combos with custom types that only represent valid states.

-- ❌ isLoading=False + data=Nothing is a nonsense state
type alias Model = { isLoading : Bool, data : Maybe Data }

-- ✓ Only valid states can be represented
type RemoteData = Loading | Loaded Data

Parse, don’t validate

Return a validated type from your parser instead of a boolean check — the type system then guarantees correctness downstream.

-- ❌ After validation you still have the raw input type
validate : UserInput -> Bool

-- ✓ Parser returns a known-valid type
parse : UserInput -> Result String ValidUser

Data Flow

Patterns for moving and transforming data through a pipeline.

Unwrap Maybe and Result early

Pattern-match at the highest level so child views work with clean, unwrapped data.

-- ❌ Every child view must handle Maybe User
userInfo : Maybe User -> Html Msg
userActivity : Maybe User -> Html Msg

-- ✓ Unwrap once, pass concrete User down
case maybeUser of
    Just user -> div [] [ userInfo user, userActivity user ]
    Nothing -> div [] [ text "No user" ]

The railway pattern

Chain operations that can fail using Result.andThen. On first failure the chain short-circuits to the error track.

process : String -> Result String TransformedData
process data =
    parseData data
        |> Result.andThen validateData
        |> Result.andThen transformData

For early-exit (not error) scenarios, define a custom andThen on your own two-track type.

Pipeline builder

Build a function by piping through validators, using Elm’s type-alias-as-constructor trick.

validateUser : User -> Result String User
validateUser user =
    Ok User
        |> validateName user.name
        |> validateAge user.age

validateName : String -> Result String (String -> a) -> Result String a
validateName name =
    Result.andThen
        (\constructor ->
            if String.isEmpty name then Err "Invalid name"
            else Ok (constructor name)
        )

Caveat: fields of the same type can be swapped silently. Keep field order in the pipeline matching the type alias definition.

Composition & Configuration

Patterns for assembling values, views, and behaviour from smaller parts.

The builder pattern

Expose a constructor with defaults plus with* modifiers so callers don’t break when new fields are added.

-- ❌ Every caller must supply every field, breaks on new fields
Button.btn { isEnabled = True, label = "Click", hexColor = "#ABC" }

-- ✓ Callers only set what they need to change
Button.newArgs "Click"
    |> Button.withHexColor "#123"
    |> Button.btn

Arguments list

Pass a list of configuration values (all the same opaque type) to a function. Used by elm/html, elm/svg, elm-css, elm-ui.

-- Functions return a common opaque type
scale : Float -> Float -> Float -> Attribute
rotation : Int -> Int -> Int -> Attribute

-- Caller builds a list
shape
    [ scale 0.5 0.5 0.5
    , position 0 -6 -13
    , rotation -90 0 0
    ]

Best when all arguments are optional and the returned type is opaque.

Combinators

Combine values of the same type to produce a value of the same type — enabling endless composition.

and : Filter -> Filter -> Filter  -- (a AND (b OR c))

Good candidates: Html, Cmd.batch, parsers, JSON decoders/encoders, filters, validations — anything tree-shaped.

Conditional rendering

Three ways to conditionally show/hide an element in view:

-- 1. Maybe + filterMap
[ Just header, maybeBanner showBanner, Just footer ]
    |> List.filterMap identity

-- 2. No-op (text "")
maybeBanner showBanner = if showBanner then bannerElement else text ""

-- 3. List concatenation
[ header ] ++ maybeBanner showBanner ++ [ footer ]
maybeBanner showBanner = if showBanner then [ bannerElement ] else []

Module Design

Patterns for encapsulation, invariants, and maintaining consistency.

Opaque types

Expose the type but not its constructor from a module. Callers must use provided functions to create and modify values.

module Lib exposing (Config, newConfig, withSize)

type Config = Config { size : Int, style : Style }

newConfig : Config
withSize : Int -> Config -> Config

Useful for hiding implementation details and preventing breaking changes in packages.

Opaque types for enforcing invariants

Use opaque types to guarantee data always satisfies a rule (e.g. a list that is always sorted).

module SortedList exposing (SortedList, new, add)

type SortedList comparable = SortedList (List comparable)

new : SortedList comparable
new = SortedList []

add : comparable -> SortedList comparable -> SortedList comparable

Only this module can add items, so the sort invariant can’t be broken from outside.

Type iterator

Keep a list of all custom-type variants in sync with the definition. Use an elm-review rule or a recursive case that the compiler checks for exhaustiveness.

-- ❌ Adding Blue won't warn that `all` is missing it
type Color = Red | Yellow | Green | Blue
all : List Color
all = [ Red, Yellow, Green ]

-- ✓ Compiler warns until Blue is handled (no wildcard!)
next : List Color -> List Color
next list =
    case List.head list of
        Nothing -> Red :: list |> next
        Just Red -> Yellow :: list |> next
        Just Yellow -> Green :: list |> next
        Just Green -> Blue :: list |> next
        Just Blue -> list

Never use _ wildcard matching in the next function — it defeats the compiler check.

Advanced Types

Patterns using phantom types and type-level state machines.

Phantom types

Add an unused type variable to restrict what functions accept and return.

type Users a = Users (List User)

activeUsers : List User -> Users Active
activeUsers users = users |> List.filter .isActive |> Users

-- This view only accepts active users
activeUsersView : Users Active -> Html msg

Useful for state machines, process flows, and validation gating.

Process flow using phantom types

Enforce a state machine at the type level so you can’t skip or reorder steps.

type Step step = Step Order

type Start = Start
type OrderWithTotal = OrderWithTotal
type Done = Done

-- Transitions are type-restricted
setTotal : Int -> Step Start -> Step OrderWithTotal
adjustQuantityFromTotal : Step OrderWithTotal -> Step Done

-- Only valid flows compile
flow total order =
    start order
        |> setTotal total
        |> adjustQuantityFromTotal
        |> done

Architecture

Patterns for scaling Elm applications with The Elm Architecture (TEA).

Reusable views

Build a view function that takes message constructors and state as arguments.

type alias Args msg =
    { currentDate : Date
    , isOpen : Bool
    , onOpen : msg
    , onClose : msg
    , onSelectDate : Date -> msg
    }

calendar : Args msg -> Html msg

Use the builder pattern for complex reusable views with many optional settings.

Nested TEA

Break a large app into child modules, each with its own Model, Msg, update, view. The parent wraps the child’s Msg and uses Html.map.

-- Parent
type Msg = Increment | Sub Sub.Msg

update msg model =
    case msg of
        Sub subMsg ->
            { model | subModel = Sub.update subMsg model.subModel }
        ...

view model =
    div []
        [ ...
        , Sub.view model.subModel |> Html.map Sub
        ]

Use sparingly — adds boilerplate and makes child-to-parent communication harder.

Child outcome

When using Nested TEA, return a third value from the child’s update to communicate results to the parent.

type Outcome = OutcomeNone | OutcomeDateUpdated Date

update : Msg -> Model -> ( Model, Cmd Msg, Outcome )

The parent inspects the outcome and reacts.

Translator

Make child views generic so they can produce messages for both themselves and their parent — no more Html.map limitation.

-- Child: view takes a toSelf constructor and any parent messages
type alias Args msg =
    { toSelf : Msg -> msg
    , onSave : msg
    }

view : Model -> Args msg -> Html msg

-- Parent wires it up
Child.view model.childModel
    { toSelf = ChildMsg
    , onSave = OnSave
    }

Global actions

When deeply nested modules need to communicate with the root (e.g. show notification, sign out), return a list of actions alongside the model and commands.

type Action = OpenSuccessNotification String | SignOut | ...

update : Msg -> Model -> ( Model, Cmd Msg, List Action )

The root update processes the action list. Mimic Cmd.batch for combining actions, and add Actions.map for actions that carry a child message.

The effects pattern

Replace Cmd Msg in update with a custom Effect type for testability.

type Effect = SaveUser User | LogoutUser | LoadData | ...

update : Msg -> Model -> ( Model, List Effect )

-- Convert to real Cmd only at the app boundary
runEffects : List Effect -> Cmd Msg

This lets you inspect and assert on effects in tests. Used by elm-program-test.

Update return pipeline

Break complex update branches into small single-concern functions piped with andThen.

SeeReport report ->
    ( model, Cmd.none )
        |> andThen (setStageToReportVisible report)
        |> andThen loadMoreDataIfNeeded
        |> andThen addKeyInUrl
        |> andThen trackSeeReportEvent

andThen : (model -> (model, Cmd msg)) -> (model, Cmd msg) -> (model, Cmd msg)
andThen fn ( model, cmd ) =
    let ( nextModel, nextCmd ) = fn model
    in ( nextModel, Cmd.batch [ cmd, nextCmd ] )

Each pipeline function handles one concern, making update branches readable and testable.


Patterns compiled from the Elm community. Originally inspired by Elm Patterns by Sebastian Porto.