按源仓库内容呈现,保留标题、案例、代码、表格、链接以及原文引用的演示图片。
ClojureScript
ClojureScript (CLJS) is Clojure that compiles to JavaScript via the Google Closure Compiler. It shares Clojure's syntax, immutable data structures, and REPL-driven workflow, but runs on JS engines (browser, Node.js) and interoperates directly with JS objects, functions, and npm/Closure libraries.
This skill covers writing idiomatic CLJS, JS interop, dependency/build configuration, and the newer native async/await support. When in doubt about compiler flags or interop mechanics, check references/ rather than guessing — CLJS has a lot of build-tooling surface area that's easy to get subtly wrong.
Not for nbb. Babashka/nbb runs CLJS through an SCI interpreter (no Google Closure Compiler, no:optimizations/:advanced, no externs) for fast-starting Node.js scripts — a different language surface from what this skill covers. nbb is out of scope here — consult nbb's docs and examples fornbb.ednprojects, anything run vianbb script.cljs/npx nbb.
Gotchas
- `:optimizations` defaults to `:none`, not
:advanced— a project with no explicit:optimizationssetting is running unoptimized dev output. - Symbol renaming under `:advanced` breaks untyped JS interop. "Works in dev, breaks in prod" is almost always this — fix with an externs file or string-keyed access (
goog.object/get), not by changing app logic. - `:main` under `:optimizations :none` only loads what's actually `:require`d. A side-effect-only namespace not reached by the entry namespace's transitive requires silently doesn't run — add an explicit
:requireor put it in:preloads. - `:output-to` and `:modules` are mutually exclusive, not layered —
:modulesneeds a per-module:output-toinside each module map, not a top-level one. - No arbitrary-precision numbers. CLJS numbers are all JS
number(a double) — no JVM-style BigDecimal/BigInt/ratios. Watch for precision loss on large integers. - `^:async` metadata goes on the `fn`/name only, never the arg vector, and
awaitonly works inside a function actually marked:async— a nestedfndoesn't inherit it and needs its own^:async(seereferences/async-functions.md). - An `:async` function always returns a `Promise`, even when the body looks like it returns a plain value.
- Truthiness differs from JS. Only
falseandnilare falsy —0,"",js/NaN,[], and(array)are all truthy.(if 0 "yes" "no")→"yes", unlike JS. Easy to get backwards when translating JS/TS conditionals. - No automatic tail-call optimization, same as JVM Clojure. CLJS functions run as ordinary JS function calls, and most JS engines don't reliably implement proper tail calls either — deep non-tail self-recursion will blow the JS call stack on large/unbounded input, even though it "works" for small test inputs. Reach for
recur(self-recursion, tail position only — see below),trampoline(mutual recursion), or wrap the recursive case inlazy-seq(sequence-producing recursion) rather than plain recursive calls whenever the input size isn't small and fixed.
Core language cheat sheet
CLJS syntax is essentially Clojure's, so standard Clojure knowledge (immutable persistent data structures, let/fn/defn, ->/->> threading, map/filter/reduce, multimethods, protocols, core.async) applies directly. For a categorized index of core-library functions when you need the exact name of something less common, see references/core-function-index.md (based on https://cljs.info/cheatsheet/). The CLJS-specific things to keep front of mind:
- Numbers: CLJS has only one numeric type backed by JS
number(a double). No arbitrary-precision integers/ratios like JVM Clojure — watch for precision issues with large integers. - Namespaces map to JS modules/Closure namespaces.
(ns my.app.core (:require [my.app.util :as u]))—:requirefor CLJS/Closure namespaces,:importfor Closure classes/enums (e.g.(:import [goog Timer])). - `defprotocol`/`deftype`/`defrecord` work like Clojure and compile to real JS constructor functions/prototypes.
- `js/` prefix accesses JS global objects:
js/console,js/document,js/Math,js/window. - No JVM interop — anything JVM-specific (
Thread,java.*classes, blocking IO) doesn't exist. Concurrency is single-threaded/event-loop based; usecore.asyncchannels or promises/async functions instead of threads or locks. - `.cljc` files hold code shared between Clojure and ClojureScript, guarded with reader conditionals:
#?(:clj (do-jvm-thing) :cljs (do-js-thing)).
JavaScript interop
- Method call:
(.methodName obj arg1 arg2)→obj.methodName(arg1, arg2) - Property access:
(.-propName obj)→obj.propName - Property set:
(set! (.-propName obj) val) - Constructor:
(SomeClass. arg1 arg2)→new SomeClass(arg1, arg2) - Static/global access:
js/JSON.stringify,js/Object.keys - Chained calls read top-down with
..:(.. js/document (getElementById "app") -innerHTML) - JS↔CLJS data conversion:
(clj->js m)and(js->clj o :keywordize-keys true) - Destructuring JS objects:
(let [{:keys [a b]} (js->clj obj :keywordize-keys true)] ...)or interop-style^jstype hints with.-accessors when you don't want a full conversion.
For consuming Closure Library, npm packages, foreign (non-Closure-compatible) JS, and CLJS libraries — including externs, advanced-compilation pitfalls, and CLJSJS — see references/dependencies-and-interop.md before guessing at a :require/:import shape or writing an externs file from scratch.
Recursion
Because CLJS has no automatic TCO (see Gotchas), which recursion tool to reach for depends on the shape of the problem:
- `recur` — self-recursion only (a function calling itself, directly, not through an intermediate function), and only from the tail position. Compiles to a real loop, so it's stack-safe for arbitrarily large input. This is the default choice for producing a scalar or a small, fixed-size result — e.g. summing a collection with
loop/recurrather than a plain recursive call. - `trampoline` — for mutual recursion (function A calls B calls A), where
recurdoesn't apply because it only optimizes self-calls. Wrap each recursive tail call in#(...)and drive the whole thing with(trampoline f args...);trampolinekeeps calling the returned function until it gets back a non-function value. Don't reach fortrampolineon a self-recursion —recuris simpler and equally stack-safe there. - `lazy-seq` — for recursion that produces a sequence, especially an unbounded/large one. Wrapping the recursive call in
lazy-seqbreaks the recursion into on-demand steps, so callers only pay for the elements they actually realize (take,first, etc.). This is usually the better fit overloop/recurwhenever the output size varies or could be large — lettake/dropon the caller side decide how much to compute. - `memoize` — trades space for time by caching by argument, useful for expensive recursive/mutually-recursive definitions with overlapping subproblems. Caveat: memoizing alone doesn't fix a stack-overflow risk — a cold cache still has to recurse all the way down on the first large call. If the recursion is deep, build the cache bottom-up by mapping the memoized function over a lazy range/seq (
(map f (range))) rather than calling it directly on a largen.
Async / promises
CLJS added native ^:async functions with an await macro (since v1.12.145) as a lighter-weight alternative to core.async for promise-based code. This is a newer feature that's easy to get wrong or not know about at all — read `references/async-functions.md` before writing or reviewing any CLJS code that deals with `Promise`s, `fetch`, or `async`/`await`-shaped logic (see Gotchas above for the two most common mistakes).
The single most common typo — ^:async sits on the function name, never the argument vector:
(defn ^:async load-user [id] (await (fetch-user id))) ; ✅ name
(defn load-user ^:async [id] ...) ; ❌ arg vector — neverIf the codebase already uses core.async (go, <!, channels) for async control flow, stay consistent with that style rather than mixing in ^:async/await unless asked to migrate.
External resources
For idiomatic-Clojure background/mindset reading (design patterns translated to Clojure idioms, core.async worked examples, FP mindset shifts) beyond this skill's own reference material, plus community-curated library lists and real-world example codebases, see references/external-resources.md. Use it as supplementary color/perspective and pointers, not as the source of truth for CLJS-specific mechanics — those stay in this skill's other reference files.
Build & compiler configuration
CLJS projects are almost always built with shadow-cljs (npm/JS-ecosystem friendly, handles npm deps automatically) or the Clojure CLI (`deps.edn`) + `cljs.main`/`cljs-build-api` combo; some older projects use Leiningen + figwheel-main. Don't assume the tool — check for shadow-cljs.edn, deps.edn, or project.clj in the project before giving setup instructions.
For the full list of compiler options (:optimizations, :target, :main, :npm-deps, :modules, :source-map, :closure-defines, warnings, etc.) with exact semantics and defaults, see references/compiler-options.md. Don't guess at option names or defaults — this is a large, easy-to-misremember surface (e.g. :static-fns defaults to false except under :advanced, :npm-deps defaults to false). See Gotchas above for the most common failure patterns tied to these options.
Working style
Default to idiomatic Clojure(Script), not a literal JS translation. This is the top priority whenever writing or rewriting CLJS, and it should win over "closest to what the JS/TS looked like" unless the user's existing codebase clearly does otherwise. Concretely, that means:
- Immutability and pure functions first. Reach for persistent data structures (maps, vectors, sets) and pure transformations over mutable state,
atoms, orset!. Only use anatom/volatile!for genuine local mutable state (e.g. component-local UI state), not as a default variable substitute. - Data over classes. Model domain data as plain maps/records rather than JS-style classes with methods, unless polymorphism genuinely calls for
defprotocol/deftype/defrecordor a JS class is required at an interop boundary (e.g. a React component). - Sequence/threading idioms over loops. Prefer
map/filter/reduce/keep/forand->/->>threading overfor/while-style imperative loops, index juggling, or manual accumulation. This is a style preference, not a stack-safety rule:loop/recuris the correct (not just a perf-justified) choice for genuinely tail-recursive, scalar-producing logic — see Recursion above — reach for a transient only when profiling justifies it. - Destructuring over manual accessors. Use
let/fn-arg destructuring ({:keys [...]},[a b & rest]) instead of repeated(get m :k)or positional indexing. - Small composed functions over long procedural bodies. Break logic into small, named, testable functions and compose them, rather than one large function with sequential mutation-heavy steps.
- Idiomatic control flow. Prefer
cond/case/condpandwhen/if-let/when-letover nestedifs or JS-style early-return chains; prefer^:async/awaitorcore.async(per the async section above) over manual.thenchains. - Namespaced keywords and data-driven design where they fit naturally (e.g.
:person/namein shared/library code) rather than plain strings for map keys.
When converting JS/TS to CLJS specifically: re-derive the idiomatic CLJS shape of the logic rather than transliterating line-by-line — e.g. turn .then chains into ^:async/await or core.async, turn classes into defrecord/protocols or plain maps + functions, turn for-loops with accumulator variables into reduce or into.
If the surrounding codebase has an established (even if less idiomatic) convention, match it for consistency rather than unilaterally "fixing" the whole file — but say so, and offer the idiomatic alternative as a suggestion rather than silently picking one or the other.
Other working-style notes:
- When debugging, ask whether the error is happening under
:none(dev) or:advanced/:simple(prod) optimizations — the failure modes are very different (the former is usually a logic/require bug, the latter is very often a renaming/externs issue).
Benchmark
Scenario: .benchmarks/scenarios/clojurescript-001-async-await.md · Run: 2026-08-31 (salience re-run wf_9a5588bc) · Log: .benchmarks/runs/2026-08-31/clojurescript-001-async-await.json
| Model | Without | With | Delta |
|---|---|---|---|
| claude-opus-4-8 | 67% | 100% | +33% |
| claude-sonnet-4-6 | 83% | 100% | +17% |
| claude-haiku-4-5 | 100% | 100% | +0% |
PASS (run 2026-08-31). Salience re-run (✅/❌^:async-placement pair added to the Async section, wf_9a5588bc): sonnet's arg-vector typo cleared (+17); gains on all models. The prior single-run sonnet −17 was a salience gap, not noise. Gate per.agents/skills/skill-optimizer/rules/release-gates.md.

