Creating and Validating JavaScript Objects in ClojureScript
Create plain JavaScript objects in ClojureScript with #js literals, read nested properties safely using goog.object, and isolate mutation at the interop boundary.
13 Apr 2026, 18:35 UTC

When a ClojureScript application calls a JavaScript library, the most common failure is not a compilation error but a silent mismatch: the library receives a Clojure map and behaves as if it were empty. Clojure maps use persistent, immutable internal structures that JavaScript property access cannot read. The practical fix is to construct genuine JavaScript objects at the boundary, access them safely with the Google Closure goog.object helpers, and keep mutation confined to a thin interop layer.
This guide shows how to create plain JavaScript objects, read nested properties without TypeError crashes, and verify that the object you pass across the boundary is the shape the external API expects.
Prerequisites
- A ClojureScript build configured with
shadow-cljsorfigwheelso thatgoog.objectand the Closure compiler are available. - A REPL connected to a browser or Node.js target for interactive checks.
- Browser developer tools or a Node console to inspect object identity.
Creating Plain JavaScript Objects
The #js reader literal tells the ClojureScript compiler to emit a JavaScript object literal instead of a Clojure map. Use it whenever a third-party function expects a configuration or options object.
(ns app.config
(:require [goog.object :as obj]))
;; A plain JS object, not a Clojure map
(def settings #js {:api-key "abc-123"
:timeout 5000})
If you need an object with a specific prototype, or an empty object that you will populate dynamically, use js/Object.create. This is useful when a library expects instances of a particular class or when you are implementing a prototype-based pattern.
(def proto #js {:base-url "https://api.example.com"})
(def instance (js/Object.create proto))
(set! (.-status instance) "active")
Reading Properties Safely with goog.object
Direct property access with .-prop throws a TypeError if the object is null or undefined. The goog.object namespace provides guarded accessors that return nil instead of crashing.
For a single key, use obj/get:
(def response #js {:data #js {:user #js {:name "Alice"}}})
;; Returns "Alice"
(obj/get response "data")
For a nested path where intermediate objects may be missing, use obj/getValueByKeys. It walks the path and returns nil if any level is absent.
;; Returns "Alice"
(obj/getValueByKeys response "data" "user" "name")
;; Returns nil instead of throwing
(obj/getValueByKeys response "data" "settings" "theme")
Mutating Objects at the Interop Boundary
Some JavaScript APIs require you to modify a state object in place. Because set! and js/Object.assign bypass ClojureScript's immutability guarantees, isolate these calls to the edge of your system and avoid sharing the mutated object with pure application logic.
To merge updates into an existing object, use js/Object.assign. It mutates the first argument and returns it.
(def settings #js {:theme "light" :notifications true})
(def updates #js {:theme "dark"})
;; Mutates 'settings' in place
(js/Object.assign settings updates)
;; settings is now {:theme "dark", :notifications true}
To set a single property, use set! with the .- accessor:
(set! (.-theme settings) "dark")
Expected Checks
Before passing an object to an external library, confirm that it is a JavaScript object and not a Clojure map. Run these checks in your REPL.
- Type check:
(type settings)returnsjs/Objectfor a JavaScript object. A Clojure map returnscljs.core/PersistentArrayMap(orPersistentHashMapin older runtimes). - Mutation check: Create a JavaScript array with
#jsand call an instance method. If the reference changes, you are working with a real JS array.
(def my-list #js [1 2])
(.push my-list 3)
;; my-list is now #js [1 2 3]
In the browser developer tools, expand the logged object. A Clojure map displays internal fields such as meta and array-node buckets; a plain JavaScript object displays only the keys you set.
Recovery Options
If a JavaScript function reports that your object is empty or undefined, the most likely cause is that you passed a Clojure map. Convert it with #js or by constructing a new object and copying keys with goog.object/set.
If you see a TypeError such as "Cannot read properties of undefined," wrap the access in obj/get or obj/getValueByKeys rather than adding nil checks at every call site.
If mutation is causing unexpected state changes elsewhere, clone the object before handing it to the library: (js/Object.assign #js {} original). This creates a shallow copy that the library can mutate safely.
Limitations and Risks
- Immutability loss:
set!andjs/Object.assignintroduce mutable state. Treat the resulting object as write-only from the perspective of your ClojureScript application. - Undefined versus nil: JavaScript
undefinedis represented asnilin ClojureScript, but a missing property and an explicitundefinedvalue are indistinguishable withobj/get. Validate critical values. - Testing friction: Functions that read
js/globals directly are harder to unit test. Pass objects as arguments instead of reaching for globals inside business logic. - Version assumptions: The
goog.objectnamespace is provided by the Google Closure Library and is available in standardshadow-cljsandfigwheelbuilds. Verify the exact function names in your project's dependency version, as Closure Library releases occasionally deprecate aliases.
To check the final result, log the object immediately before the external call and inspect it in the console. If the keys match the API documentation and the type check returns js/Object, the boundary is correct.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.