ClojureScript JavaScript Interop: Using js/ and clj->js Safely
Practical guidance for calling JavaScript from ClojureScript using the js/ prefix and converting data with clj->js.
01 Sept 2025, 21:31 UTC

The Problem: ClojureScript Data Is Not JavaScript Data
You are building a ClojureScript frontend and need to initialize a JavaScript charting library. You pass a Clojure map as the configuration, but the library throws a TypeError because it expects a plain JavaScript object with properties it can read and mutate. ClojureScript maps and vectors are immutable persistent structures, not native JS objects and arrays. To work with the JavaScript ecosystem, you must cross the boundary deliberately.
The takeaway: use the js/ prefix to reach JavaScript globals, and use clj->js to convert ClojureScript collections into the plain objects and arrays that JS libraries expect.
Accessing JavaScript Globals with js/
The js/ prefix tells the ClojureScript compiler to treat an identifier as a JavaScript global rather than a ClojureScript symbol. Calling (js/console.log "message") compiles directly to console.log("message") with no wrapper overhead. This works for browser APIs such as js/document and js/window, as well as globals exposed by scripts loaded in your HTML.
Because js/ bypasses ClojureScript's namespace resolution, code that relies on it heavily is harder to test outside a browser or Node.js environment where those globals exist.
Converting ClojureScript Collections with clj->js
JavaScript libraries usually require plain objects and arrays. The clj->js function recursively converts ClojureScript maps to JS objects and vectors to JS arrays. If you pass an unconverted Clojure map to a JS function that iterates with for...in or calls .push(), the call will fail at runtime.
For finer control over an existing JavaScript object, goog.object provides utilities to read and set keys without converting the entire structure. Use clj->js when you need to create a new JS value from ClojureScript data.
Worked Example: Initializing a JS Component
This example assumes a shadow-cljs project and a JavaScript library exposed as a global ChartManager. First, create the project if you do not have one.
# Run in your terminal, not in the ClojureScript REPL
# Requires: shadow-cljs installed and available on PATH
# Placeholder: [project-name] is your project directory
npx shadow-cljs init [project-name]
# Expected check: deps.edn and shadow-cljs.edn appear in the directory
# Risk: existing files with the same names may be overwritten
Run the ClojureScript code in your application namespace after the library script has loaded.
;; Run in a shadow-cljs ClojureScript namespace
;; Requires: ChartManager global available in the browser environment
;; Permissions: None beyond normal page execution
(ns app.chart
(:require [goog.object :as goog.object]))
(defn init-chart [element-id data-points]
(let [config (clj->js {:type "line"
:responsive true
:data {:labels ["Jan" "Feb" "Mar"]
:datasets [{:label "Growth"
:data (clj->js data-points)}]}})]
(js/ChartManager.init (js/document.getElementById element-id) config)))
Verification: open the browser developer console, run (init-chart "chart-canvas" [10 20 15]), and inspect the target element. If the library rendered a chart, the interop succeeded. If you omitted clj->js, the library likely throws a TypeError when it tries to treat the Clojure map as a JavaScript object.
Trade-offs and Limitations
- Immutability is lost at the boundary. A JavaScript library may mutate the object returned by
clj->js, and those changes are visible to any ClojureScript code holding the same reference. - Type mismatches cause runtime errors. ClojureScript vectors are not JavaScript arrays, so libraries expecting
.lengthor.push()will fail unless you convert first. - Global dependencies make testing harder. Code calling
js/directly requires mocks or a real browser environment to run in isolation.
Keep Interop at the Boundary
Do not scatter js/ calls through business logic. Create a small boundary layer of wrapper functions that convert ClojureScript data to JavaScript, call the library, and return results. The rest of your application can remain pure and functional while the interop stays isolated and easier to test.
To verify your setup, create a minimal shadow-cljs project, add a script tag that exposes a test global, and confirm that (js/console.log "interop works") prints to the browser console without compiler warnings.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.