Calling JavaScript from PureScript with the Foreign Function Interface
Learn how to expose JavaScript functions to PureScript using the Foreign Function Interface (FFI). Follow a step‑by‑step guide with a Lodash example, setup details, and trade‑off discussion.
20 Feb 2026, 09:15 UTC

Problem: PureScript Meets Existing JavaScript Libraries
PureScript is a pure, strongly‑typed language that compiles to JavaScript. When you need to tap into the vast ecosystem of existing JavaScript libraries—say, the popular lodash utility library—the question is: how do you keep the type safety of PureScript while calling untyped JavaScript code?
Thesis: FFI is the Most Straight‑Forward and Type‑Safe Bridge
The Foreign Function Interface (FFI) in PureScript gives you a declarative, type‑checked path to call JavaScript functions. By annotating external functions with @js and writing concise type signatures, you can invoke JavaScript code while retaining PureScript’s guarantees about purity and referential transparency.
1. Setup: Project, Dependencies, and Bundler Configuration
Start with a fresh PureScript project:
purs init my-ffi-demo
cd my-ffi-demo
Add the purescript-effect package (it’s included by default in new projects) and install lodash via npm:
npm install lodash
Because PureScript compiles to JavaScript, you’ll eventually bundle the output with a tool like Webpack, Rollup, or esbuild. The bundler must expose the lodash module to the generated code. For example, a minimal webpack.config.js could look like:
module.exports = {
entry: './output/index.js',
output: { filename: 'bundle.js', path: __dirname + '/dist' },
mode: 'development',
resolve: { extensions: ['.js'] }
};
When you run purs compile, PureScript writes the compiled JavaScript to output/. The bundler will then package that output together with lodash from node_modules.
2. Declaring External Functions with @js
PureScript’s FFI uses the @js pragma to tell the compiler that a function lives in JavaScript. The syntax is concise:
-- | MyJS.purs
module MyJS where
foreign import unsafe uniq :: forall a. Array a -> Effect (Array a)
Explanation of the parts:
foreign import– indicates an external declaration.unsafe– marks the call as potentially impure; the compiler can’t guarantee safety.uniq– the PureScript name you’ll use.:: forall a. Array a -> Effect (Array a)– a type signature that mirrors the JavaScript function’s shape. TheEffectmonad represents a computation that may perform side effects.
In the accompanying JavaScript file, expose the function in the global namespace (for simplicity in this example):
// MyJS.js
export function uniq(arr) {
return _.uniq(arr);
}
Make sure the bundler knows to load MyJS.js so that the global symbol is available when the PureScript code runs.
3. Using the Imported Function in PureScript
Now you can use uniq just like any other PureScript function:
-- | Main.purs
module Main where
import Prelude
import Effect (Effect)
import Effect.Console (log)
import MyJS (uniq)
main :: Effect Unit
main = do
let list = [1,2,2,3,3,3,4]
uniqed <- uniq list
log $ "Unique list: " ++ show uniqed
When you compile and run this code (e.g., node dist/bundle.js), the output should be the JavaScript result of _.uniq(list). The PureScript compiler guarantees that the types line up; if you accidentally pass a non‑array, the compiler will reject it before you hit the JavaScript runtime.
4. Error Handling and Runtime Checks
FFI functions are marked unsafe because the compiler cannot enforce JavaScript’s runtime behavior. To mitigate surprises:
- Validate inputs in PureScript before passing them to the external function.
- Wrap the FFI call in a
tryCatchfrompurescript-effectif you expect the JavaScript side to throw. - Document the expected JavaScript contract so that downstream developers know what invariants to maintain.
5. Trade‑Offs and Limitations
- Runtime Requirement – PureScript code that uses FFI must run in a JavaScript environment. PureScript alone cannot execute without a JS engine.
- Bundler Overhead – Each FFI declaration adds a dependency that the bundler must resolve. Misconfigured module paths can lead to runtime failures that are hard to trace.
- Safety vs. Convenience – The
unsafekeyword tells the compiler to skip certain checks. While this is necessary for interop, it also means you lose some of PureScript’s safety guarantees. - Version Compatibility – The
Effectmonad is the current standard, but older projects may still useIO. Ensure your codebase and tooling are aligned.
Actionable Takeaway
To integrate a JavaScript library into a PureScript project:
- Install the library via npm.
- Write a thin JavaScript wrapper that exposes the desired functions.
- Declare those functions in PureScript with
foreign import unsafeand a matching type signature. - Bundle the output together, ensuring the wrapper module is included.
- Handle potential runtime errors with
tryCatchor pre‑validation.
By following these steps, you keep your PureScript code type‑safe while still leveraging the rich ecosystem of JavaScript libraries.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.