Bridging the Gap: Managing JavaScript Interop in ClojureScript
Learn how to effectively use the js/ prefix, #js literals, and goog.object to bridge the gap between ClojureScript's immutability and JavaScript's native objects.
15 Aug 2025, 04:31 UTC

The Impedance Mismatch of JS Interop
When building a ClojureScript application, you eventually hit a wall: a specialized JavaScript library that doesn't have a Clojure wrapper. The challenge is not just calling the function, but managing the data. If you pass a Clojure map to a JS library expecting a plain object, the library will likely fail silently or throw an error because it cannot find the keys inside Clojure's internal map structure.
The goal is to move data across the boundary between ClojureScript's immutable world and JavaScript's mutable world without introducing runtime crashes or unpredictable state.
Direct Global Access with the js/ Prefix
The fastest way to reach into the JavaScript environment is the js/ prefix. This tells the compiler to look for the identifier in the global namespace of the current environment (such as window in the browser or global in Node.js).
For example, js/console.log allows you to print directly to the developer console. This is a syntactic shortcut; it does not wrap the JS object in a Clojure-friendly way, but provides a direct reference to the native JS entity.
Creating Native Objects with #js
Clojure maps are not JavaScript objects. To create a structure that a JS library can actually read, you must use the #js literal. This creates a native, mutable JavaScript object.
;; This is a Clojure map (Immutable)
(def cljs-map {:name "ReadMeFeed" :version 1})
;; This is a native JS object (Mutable)
(def js-obj #js {:name "ReadMeFeed" :version 1})
Using #js is critical when passing configuration objects to third-party libraries. If you use a standard map, the JS library will see a complex Clojure object rather than a simple key-value store.
Dynamic Access via goog.object
Sometimes you don't know the key of a JS object at compile time, or you are dealing with a library that returns objects with dynamic properties. Since native JS objects don't support Clojure's get or assoc functions, you need goog.object.
The goog.object library provides utility functions to treat JS objects like maps. This is particularly useful for reading values from a JS API response where the keys might contain characters that are invalid as Clojure keywords.
Worked Example: Integrating a Generic JS Library
Assume we are using a hypothetical JS library called ChartLib that requires a configuration object and provides a method to update data.
;; Run this in a ClojureScript environment (e.g., shadow-cljs)
;; Required: The ChartLib library must be loaded in the global scope
(defn initialize-chart []
(let [config #js {:width 500 :height 300 :theme "dark"}
chart (js/ChartLib. config)]
;; Use goog.object to safely check a property on the returned JS instance
(if (goog.object/hasProperty? chart "render")
(chart.render)
(js/console.error "ChartLib instance is missing render method"))
chart))
Verification: To verify the object type, you can run (js/Object.prototype.toString.call (initialize-chart)). It should return [object Object], confirming it is a native JS object and not a Clojure map.
Trade-offs and Safety Risks
Interop is a double-edged sword. By using js/ and #js, you are bypassing ClojureScript's compile-time safety. There are three primary risks:
- Mutability:
#jsobjects are mutable. If you pass one to a function and that function modifies it, your state changes globally, breaking the principle of immutability. - Undefined Errors: Accessing a property on a
js/global that doesn't exist will returnundefined. If you then try to call a method on thatundefinedvalue, the application will crash. - Environment Locking: Heavy use of
js/windowmakes your code impossible to run in Node.js without a shim, reducing the portability of your logic.
Actionable Summary
When integrating JavaScript: use #js for configuration objects, js/ for global API access, and goog.object for dynamic property manipulation. Always verify the existence of a method using goog.object/hasProperty? before calling it on a third-party object to avoid runtime crashes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.