Using PureScript FFI to Safely Call JavaScript Libraries
Learn how PureScript’s FFI lets you call JavaScript functions with type‑safe contracts, see a worked sqrt example, and understand the limits and verification steps.
24 Jul 2026, 11:08 UTC

Problem: Needing JavaScript while staying in PureScript
When a PureScript project must interact with an existing JavaScript API—for example, a browser‑only charting library or a Node.js module—the naïve approach is to use unsafe inline JavaScript. This bypasses the compiler’s type guarantees and can lead to runtime errors that are hard to trace.
Thesis: FFI gives type‑safe interop when the contract is correct
PureScript’s Foreign Function Interface (FFI) lets you declare JavaScript values with explicit PureScript types. The compiler erases the foreign import at build time, expecting a matching JavaScript export. If the export matches the declared type, calls are type‑checked; if not, the failure appears at runtime, making the contract explicit.
How FFI works
A foreign import looks like a regular function definition but is prefixed with foreign import. The PureScript side provides only a type signature; the implementation lives in a separate JavaScript file that the compiler does not inspect.
-- src/MathOps.purs
module MathOps where
foreign import sqrt :: Number -> Number
The corresponding JavaScript must export a property named sqrt that accepts a number and returns a number:
-- src/MathOps.js
exports.sqrt = function (x) { return Math.sqrt(x); };
When you compile the module (purs src/MathOps.purs -m MathOps -o output.js), the generated JavaScript contains a call to MathOps.sqrt that directly invokes the exported property.
Worked example: wrapping Math.sqrt
- Set up the toolchain (run in a terminal with npm access):
npm install -g purescript@0.15.8 purs@0.15.8 - Initialize a project:
mkdir purescript-ffi-demo && cd purescript-ffi-demo purs init - Add the PureScript module (
src/MathOps.purs) as shown above. - Add the JavaScript implementation (
src/MathOps.js) as shown above. - Compile:
purs src/MathOps.purs -m MathOps -o output.js - Run with Node to verify:
Expected check: the printed value should benode -e "const {sqrt} = require('./output.js'); console.log(sqrt(9));"3. If the JavaScript file exported a string instead of a number, the program would throw at runtime, illustrating the contract dependence.
Trade‑offs and limitations
- Safety relies on the JavaScript side: the PureScript compiler cannot verify that the exported value matches the declared type. A mismatch yields a runtime error, not a compile‑time error.
- Toolchain version sensitivity: changes between PureScript releases (e.g., 0.15 → 0.16) may adjust FFI syntax or module system expectations (CommonJS vs. ES modules). Locking the
pursversion inpackage.jsonor using a reproducible build script mitigates surprises. - Boilerplate for each library: to keep the FFI layer thin, developers typically create a small PureScript package that re‑exports safe wrappers around the foreign imports.
Actionable closing
When you need to call JavaScript from PureScript, treat the FFI as a contract:
- Write a precise PureScript type signature for each JavaScript value you intend to use.
- Provide a matching JavaScript export; test it in isolation with Node or a browser console.
- Compile and run a small sanity check (as in the worked example) before integrating the module into the larger codebase.
- Document the required
pursversion and keep the JavaScript file alongside the PureScript source so future maintainers see the contract clearly.
By following these steps you gain the reuse of the JavaScript ecosystem while preserving the type‑safety guarantees that make PureScript appealing for larger applications.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.