Using PureScript’s Row‑Polymorphic Records to Write Flexible, Type‑Safe Functions
Learn how PureScript’s row‑polymorphic extensible records let you accept varied record shapes without duplicating types, with a concrete example and trade‑offs.
02 Jul 2025, 13:24 UTC

Problem: handling varied record shapes without duplicating types
When you receive JSON payloads or UI props that share a few common fields but may carry extra data, you often face a choice: define a separate data type for each shape, or use a overly‑general type that loses compile‑time guarantees. Both approaches add boilerplate or weaken safety.
Thesis: PureScript’s row‑polymorphic extensible records let a single function accept any record that contains at least the fields it needs, preserving type safety while avoiding duplicate types.
How row polymorphism works
In PureScript a record type can end with a row variable, written as { name :: String | r }. The | r part stands for "the rest of the row" and can be instantiated with zero or more additional fields. A function annotated with such a type can be called with any record that has at least a name :: String field, regardless of other fields.
Worked example
module Main where
import Prelude
import Effect (Effect)
import Effect.Console (log)
greet :: { name :: String | r } -> String
greet rec = "Hello, " ++ rec.name ++ "!"
main :: Effect Unit
main = do
log $ greet { name: "Ada" }
log $ greet { name: "Bob", age: 30 }
log $ greet { name: "Cox", title: "Engineer" }
The function greet only requires a name field. The three records passed to it each satisfy that requirement, even though they contain different extra fields (age, title).
To try this yourself:
- Create a new PureScript project:
spago init(requiresspagoandpurson your PATH). - Add the module above as
src/Main.purs. - Build the project:
spago build(orpurs compile src/Main.purs -o output/Main.js). - Run the generated JavaScript with Node:
node output/Main.js.
If the build succeeds and the Node execution runs without type errors, you have confirmed that the function accepts each record shape.
Trade‑offs and limitations
- Type‑error messages: Because the compiler must reason about row variables, error output can become verbose, especially when a function is miss‑applied.
- Learning curve: Developers new to kind‑level row variables need to understand the
| rsyntax and how it interacts with inference. - Inference pressure: Over‑using row‑polymorphic records in large code bases can make it harder for the compiler to infer precise types, sometimes requiring explicit type annotations.
As a practical guideline, expose only the fields a function truly needs in its type signature. Adding unnecessary fields reduces the polymorphism benefit and can make the code harder to refactor.
Actionable closing
Start by identifying functions that currently accept a concrete record type but only read a subset of its fields. Replace the concrete type with a row‑polymorphic version that lists exactly those required fields. Re‑compile and verify that existing call sites still work. Over time, this pattern reduces duplicate types and keeps your codebase both flexible and type‑safe.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.