Using Row‑Polymorphic Records for Flexible PureScript UI Props
Learn how PureScript’s row‑polymorphic extensible records let UI components require specific props while accepting extra fields, giving both flexibility and compile‑time safety.
05 Aug 2026, 06:46 UTC

The problem with UI prop types
When you write a reusable component in PureScript you face a trade‑off: make the prop type an exact record and reject any extra fields, or use a generic record and lose compile‑time guarantees about required fields. Both approaches hurt either flexibility or safety.
Thesis: row‑polymorphic extensible records give both
PureScript’s row polymorphism lets you declare exactly which fields a component needs while still accepting additional fields. The extra fields are captured in a row variable, preserving type safety without forcing callers to omit useful data.
How it works
A row‑polymorphic type looks like { label :: String, onClick :: Effect Unit | r }. The pipe (|) separates the required fields (label and onClick) from the row variable r, which stands for “zero or more other fields”. Any record that contains at least those two fields matches the type, regardless of what else it holds.
Component signature
button :: forall r. { label :: String, onClick :: Effect Unit | r } -> HTML r
button props = ...
The function returns HTML r so the extra fields flow through to the generated element (useful when you later spread them onto a DOM node).
Worked example
Define a simple button component that only cares about label and onClick:
module Main where
import Prelude
import Effect (Effect)
import HTML (HTML, button_, text, onClick)
button :: forall r. { label :: String, onClick :: Effect Unit | r } -> HTML r
button { label, onClick } = button_ [ onClick onClick ] [ text label ]
main :: Effect Unit
main = pure ()
Now use the component with an extra field disabled that the button does not inspect:
example :: HTML ( disabled :: Boolean | unit )
example = button { label: "Submit", onClick: \_ -> pure (), disabled: true }
The type of example shows that the disabled field is preserved in the row (disabled :: Boolean | unit). If you try to call button without a label field you get a clear error:
-- This fails to compile:
bad = button { onClick: \_ -> pure () }
-- Error:
-- Could not match type
-- { label :: String, onClick :: Effect Unit | r0 }
-- with type
-- { onClick :: Effect Unit | r1 }
-- The type variable r0 cannot be solved because the label field is missing.
Trade‑offs and limitations
- Error messages can become longer because the compiler prints the whole row variable when a field is missing or extra.
- You need a library that exposes the row kind for the output type (e.g.,
purescript-htmlorpurescript-react). Most mainstream UI libraries already do. - Keeping component signatures small (few required fields) reduces the chance of confusing row inference.
These drawbacks are mild compared to the gain: you get reusable components that are both type‑safe and extensible.
Actionable closing
Start by adding a row variable to any prop type that currently uses an exact record. Enable the -XRowPolymorphism flag (it is on by default in recent PureScript releases) and verify that your code still compiles. Then try passing an extra field; you should see it flow through the output type without any change to the component implementation. If you encounter confusing errors, break the signature into smaller pieces or add type annotations to intermediate values. Over time you’ll find that row‑polymorphic records make your UI library safer to compose and easier to extend.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.