PureScript: Strictly Typed Language Compiled to JS
PureScript is a small, strongly‑typed functional language that compiles directly to JavaScript. It brings the discipline of Haskell’s type system to the browser and Node.js, while keeping the output size and runtime characteristics of plain JavaScript. If you’ve ever struggled with runtime type errors or wish you could catch more bugs at compile time, PureScript offers a compelling alternative. In this article we’ll explore its type system, set up a development environment, and walk through a couple of real‑world examples that show why PureScript is gaining traction in the JavaScript ecosystem.
Why Choose PureScript?
PureScript’s core strength lies in its static type system, which guarantees that many classes of bugs simply cannot compile. Types are inferred, so you don’t have to annotate everything, yet you retain the safety net of compile‑time checks. The language is also pure: functions have no side effects unless you explicitly opt into them, making reasoning about code much easier.
Another advantage is seamless interop with existing JavaScript libraries. Because PureScript compiles to idiomatic ES modules, you can import npm packages, call them from PureScript, and even expose PureScript functions back to JavaScript. This makes migration incremental—no need for a full rewrite.
Finally, the tooling around PureScript is mature enough for production use. The compiler provides helpful error messages, the spago package manager handles dependencies, and IDE extensions offer real‑time type checking.
Core Type System Features
Algebraic Data Types (ADTs)
ADTs let you model data precisely. A Maybe type, for instance, captures the presence or absence of a value without resorting to null or undefined. This eliminates a whole class of null‑reference errors.
Type Classes
Borrowed from Haskell, type classes enable ad‑hoc polymorphism. The Eq type class defines equality, while Functor abstracts over mappable structures. By implementing these interfaces, you can write generic, reusable code.
Row Polymorphism
Rows allow you to work with extensible records. You can add or remove fields while preserving type safety, which is perfect for handling JSON objects that may evolve over time.
Higher‑Kinded Types
PureScript supports higher‑kinded types, letting you abstract over type constructors like Array or Maybe. This is the foundation of powerful abstractions such as monads and applicatives.
Getting Started
First, install the compiler and the spago package manager:
npm install -g purescript spago
Next, create a new project with a single command:
spago init
This scaffolds a src/Main.purs file, a test folder, and a spago.dhall configuration. Run spago build to compile, and spago run to execute the generated JavaScript.
Example 1: A Simple Typed Calculator
Let’s build a tiny calculator that evaluates arithmetic expressions. The goal is to demonstrate algebraic data types, pattern matching, and type safety.
module Calculator where
import Prelude
-- Define the expression AST
data Expr
= Lit Number
| Add Expr Expr
| Sub Expr Expr
| Mul Expr Expr
| Div Expr Expr
-- Evaluate an expression safely
eval :: Expr -> Either String Number
eval (Lit n) = Right n
eval (Add a b) = (+) <$> eval a <*> eval b
eval (Sub a b) = (-) <$> eval a <*> eval b
eval (Mul a b) = (*) <$> eval a <*> eval b
eval (Div _ (Lit 0)) = Left "Division by zero"
eval (Div a b) = (/) <$> eval a <*> eval b
The Either type forces us to handle division‑by‑zero at compile time, preventing a runtime exception. You can test the calculator in the REPL:
> eval (Div (Add (Lit 10.0) (Lit 5.0)) (Lit 0.0))
Left "Division by zero"
Notice how the type checker guarantees that every branch of eval returns an Either String Number. No unchecked NaN or Infinity can slip through.
Example 2: Fetching Data with Aff
PureScript’s Aff monad provides a principled way to work with asynchronous effects, similar to JavaScript’s Promise but with type safety baked in. Below is a minimal example that fetches a JSON placeholder post and decodes it using purescript‑argonaut.
module FetchPost where
import Prelude
import Effect (Effect)
import Effect.Aff (Aff, launchAff_)
import Effect.Class.Console (log)
import Data.Either (Either(..))
import Data.Argonaut (decodeJson, jsonParser)
import Data.Argonaut.Decode (class DecodeJson)
import Data.Argonaut.Decode.Generic (genericDecodeJson)
import Data.Generic.Rep (class Generic)
import Data.HTTP (get)
import Data.HTTP.Request (Request(..), Method(..))
import Data.HTTP.Response (Response(..))
import Data.HTTP.Status (statusCode)
type PostId = Int
newtype Post = Post
{ userId :: Int
, id :: Int
, title :: String
, body :: String
}
derive instance genericPost :: Generic Post _
instance decodePost :: DecodeJson Post where
decodeJson = genericDecodeJson
fetchPost :: PostId -> Aff (Either String Post)
fetchPost pid = do
let url = "https://jsonplaceholder.typicode.com/posts/" <> show pid
resp <- get url
case statusCode resp of
200 -> pure $ case jsonParser resp.body of
Right json -> decodeJson json
Left err -> Left ("JSON parse error: " <> err)
_ -> pure $ Left ("HTTP error: " <> show (statusCode resp))
main :: Effect Unit
main = launchAff_ do
result <- fetchPost 1
case result of
Right post -> log $ "Fetched title: " <> post.title
Left err -> log $ "Failed: " <> err
This snippet showcases several PureScript strengths:
- Typed HTTP requests: The
getfunction returns a typedResponse, so you can inspect status codes safely. - JSON decoding: The
DecodeJsoninstance is derived automatically, eliminating boilerplate parsers. - Effect tracking: The
Affmonad makes it clear which functions perform side effects, keeping the rest of the code pure.
Running spago run prints the title of post #1, proving that PureScript can replace a typical fetch + .then() chain with a concise, type‑checked workflow.
Real‑World Use Cases
Many companies have adopted PureScript for front‑end and back‑end projects where reliability outweighs the learning curve. Below are three common scenarios where PureScript shines.
- Complex UI State Management: Libraries like
purescript-reactandpurescript-halogenlet you model UI state as immutable records, drastically reducing bugs caused by mutable state. - Serverless Functions: PureScript compiles to a single JavaScript file, making it ideal for AWS Lambda or Cloudflare Workers where bundle size matters.
- Data‑Intensive Pipelines: The strong type system catches schema mismatches early, which is valuable in ETL jobs that transform JSON or CSV data.
In each case, the guarantee that “if it compiles, it works” translates into fewer runtime incidents and smoother deployments.
Performance and Interop
PureScript’s generated JavaScript is deliberately straightforward—no hidden runtime, just plain functions and objects. Benchmarks show that PureScript code runs within 5‑10% of hand‑written JavaScript for typical arithmetic and data‑processing tasks. The slight overhead is often outweighed by the safety gains.
Interop is achieved through foreign import declarations, which let you call any JavaScript function as if it were a PureScript function. Conversely, foreign export lets you expose PureScript functions to JavaScript, enabling a gradual migration strategy.
foreign import consoleLog :: String -> Effect Unit
foreign export logMessage :: String -> Effect Unit
logMessage = consoleLog
These bindings are type‑checked, so you cannot accidentally pass a number where a string is expected, a common source of bugs in vanilla JavaScript.
Pro Tips for Production
Tip 1: Keep your
spago.dhalldependencies locked to specific versions. PureScript’s ecosystem evolves quickly, and version mismatches can cause subtle compilation failures.Tip 2: Use
purs-tidyto enforce a consistent code style. A uniform layout makes pattern matching and type signatures easier to read across large codebases.Tip 3: When integrating with a large JavaScript codebase, isolate the PureScript modules behind a thin façade. Export only the functions you need, and treat the rest of the code as a black box. This minimizes the surface area for type‑mismatch errors.
Testing PureScript Code
PureScript supports property‑based testing via the purescript-quickcheck library. Because functions are pure by default, you can write deterministic tests without needing mocks or stubs. Here’s a quick example that verifies the commutativity of addition for our calculator:
module CalculatorSpec where
import Prelude
import Test.QuickCheck (quickCheck)
import Calculator (Expr(..), eval)
prop_addComm :: Number -> Number -> Boolean
prop_addComm a b =
eval (Add (Lit a) (Lit b)) == eval (Add (Lit b) (Lit a))
main :: Effect Unit
main = quickCheck prop_addComm
Running the test suite with spago test will generate random numbers and confirm the property holds, giving you confidence that your core logic behaves as expected.
Tooling Ecosystem
The PureScript ecosystem includes several first‑class libraries:
- purescript-prelude: A minimal standard library that encourages explicit imports.
- purescript-foreign: Helpers for safe FFI bindings.
- purescript-halogen: A UI library that enforces a unidirectional data flow, similar to Elm.
- purescript-aff: Asynchronous effect handling with cancellation support.
Most of these libraries follow the same design philosophy: pure core logic with effectful operations isolated behind well‑typed abstractions. This uniformity reduces the cognitive load when switching between projects.
Community and Learning Resources
The PureScript community is active on Discord, the PureScript Discourse forum, and the #purescript channel on Libera.Chat. Official documentation, tutorials, and the “PureScript by Example” book are excellent starting points. For developers coming from JavaScript, the “From JavaScript to PureScript” series highlights the most common pitfalls and migration patterns.
Contributing to open‑source PureScript packages is a great way to deepen your understanding. Many libraries welcome pull requests that add missing type class instances or improve documentation, providing low‑risk entry points for newcomers.
Conclusion
PureScript offers a compelling blend of strict static typing, functional purity, and seamless JavaScript interop. By catching errors at compile time, it reduces runtime crashes and improves developer confidence. Whether you’re building a single‑page application, a serverless function, or a data‑processing pipeline, PureScript’s type system and expressive abstractions can make your code more reliable and maintainable. With a mature toolchain, growing community, and practical examples like the typed calculator and safe HTTP fetch, it’s a language worth exploring for any JavaScript developer who values correctness without sacrificing the flexibility of the web platform.