These are built into the compiler and cannot be overridden.
+
+
if
+
(iftestthenelse)
+
Evaluates test. If truthy, evaluates and returns
+ then, otherwise else. Both branches are
+ optional (missing else returns nil).
+ Note: Squint follows ClojureScript truthiness — only nil,
+ undefined, and false are falsy.
+ 0 and "" are truthy.
+
+
let
+
(let [namevalue ...] body)
+
Creates local bindings. Bindings are sequential — later bindings
+ can reference earlier ones. Supports destructuring. Compiles to
+ const declarations. Destructuring uses
+ squint_core.get() rather than JS native destructuring.
+
+
do
+
(doexpr1expr2 ...)
+
Evaluates expressions in order, returns the last. Used to group
+ multiple expressions where only one is expected.
Converts hiccup-style vectors to JSX. Works with React and any
+ JSX-consuming library.
+
+
#html
+
#html [:div
+ [:h1"Hello"]
+ [:p"World"]]
+
+;; Hiccup shorthand for id and class
+#html [:div.container#main
+ [:p.intro"Welcome"]]
+
+;; Raw unescaped output via :$
+#html [:$"<!DOCTYPE html>"]
+
Generates HTML strings from hiccup syntax. Content is escaped by
+ default. Use [:$ ...] for raw unescaped output. Supports
+ hiccup shorthand for id (#) and class (.)
+ attributes. Useful for server-side rendering or static site
+ generation.
+
+
+
+
Standard library highlights
+
+
Squint re-implements a subset of Clojure's standard library for
+ JavaScript data structures. Here are the most commonly used
+ functions:
The bundler handles bundling, minification, and dev server
+
+
+
Squint's output is plain ES modules — no special loader or plugin
+ is needed. The bundler sees standard JavaScript.
+
+
Namespace to path mapping: Squint converts namespace names
+ to file paths — hyphens become underscores and dots become directory
+ separators. For example, namespace my-app.core compiles
+ to my_app/core.mjs.
+
+
Configure source and output directories in squint.edn:
# Terminal 1: compile Squint
+$ npx squint watch
+
+# Terminal 2: Vite dev server
+$ npx vite out
+
+
Vite's HMR (Hot Module Replacement) will pick up changes as
+ Squint recompiles your files.
+
+
esbuild
+
+
esbuild is the fastest bundler and ideal for production builds:
+
+
$ npm install esbuild --save-dev
+
+
# Bundle for the browser
+$ npx esbuild out/my_app/core.mjs \
+ --bundle --minify --outfile=dist/app.js
+
+# Bundle for Node.js
+$ npx esbuild out/my_app/core.mjs \
+ --bundle --platform=node --outfile=dist/app.cjs
+
+
esbuild produces extremely small bundles. Combined with Squint's
+ already-small output, this gives you the best possible bundle sizes.
+
+
Node.js (no bundler)
+
+
For scripts, CLI tools, and servers, no bundler is needed. Run
+ the compiled output directly:
+
+
# Compile and run directly
+$ npx squint run src/my_app/core.cljs
+
+# Or compile separately and run with Node
+$ npx squint compile src/my_app/core.cljs
+$ node out/my_app/core.mjs
+
+
Make sure your package.json has
+ "type": "module" (see below).
+
+
package.json setup
+
+
The most important setting for Squint projects:
+
+
{
+ "type": "module"
+}
+
+
Squint outputs .mjs files by default, which Node.js
+ treats as ES modules regardless of this setting. But if you
+ configure Squint to output .js files (via
+ :extension "js" in squint.edn), or if
+ other tooling expects it, "type": "module" is
+ required. It's good practice to always include it.
Squint works with any text editor — .cljs files get
+ Clojure syntax highlighting out of the box. For a richer experience
+ with inline evaluation, use nREPL integration.
Squint includes an experimental nREPL server that enables editor
+ integration:
+
+
$ npx squint nrepl-server :port 1339
+
+
This starts an nREPL server on port 1339. Your editor's Clojure
+ plugin can connect to this port for inline evaluation, code
+ completion, and interactive development.
+
+
Note: The nREPL server is experimental. Known limitation:
+ when sending multiple forms at once, some may not be evaluated.
+ Send forms one at a time for reliable results.
+
+
VS Code (Calva)
+
+
Calva is the most popular Clojure
+ extension for VS Code. To use with Squint:
+
+
+
Install the Calva extension from the VS Code marketplace.
+
Start the nREPL server: npx squint nrepl-server :port 1339
+
In VS Code, run Calva: Connect to a Running REPL Server
+ from the command palette.
+
Select "Generic" as the project type.
+
Enter the port (1339).
+
+
+
You can now evaluate expressions inline with
+ Ctrl+Enter (current form) or
+ Alt+Enter (top-level form).
Start the nREPL server:
+ npx squint nrepl-server :port 1339
+
In Emacs, run M-x cider-connect.
+
Enter localhost and port 1339.
+
+
+
Standard CIDER keybindings work: C-c C-e to evaluate
+ the expression before point. Note: C-c C-k (evaluate
+ buffer) sends multiple forms at once, which may be unreliable due
+ to the nREPL limitation above. Prefer evaluating forms one at a
+ time.
+
+
Neovim (Conjure)
+
+
Conjure connects
+ to nREPL for inline evaluation in Neovim:
+
+
+
Install Conjure via your plugin manager.
+
Start the nREPL server:
+ npx squint nrepl-server :port 1339
+
Create a .nrepl-port file containing
+ 1339 in your project root.
+
Open a .cljs file — Conjure auto-connects.
+
+
+
IntelliJ (Cursive)
+
+
Cursive provides Clojure
+ support in IntelliJ. It can connect to Squint's nREPL via the
+ "Remote REPL" configuration:
+
+
+
Start the nREPL server.
+
Create a new "Clojure REPL → Remote" run configuration.
+
Set host to localhost, port to 1339.
+
+
+
Console REPL
+
+
For quick experimentation without editor integration:
+
+
$ npx squint repl
+
+
This starts an interactive REPL in your terminal. You can also
+ use the web-based playground
+ for a richer experience with syntax highlighting, compiled output
+ viewing, and sharing.
+
+
Syntax highlighting without nREPL
+
+
Even without nREPL, most editors highlight .cljs files
+ correctly. If yours doesn't, configure it to use Clojure syntax
+ highlighting for .cljs files. The experience will be
+ similar to editing ClojureScript — the syntax is identical.
If you already know Clojure or ClojureScript, this guide covers
+ everything that's different in Squint. If something isn't mentioned
+ here, it probably works the way you'd expect.
atom, swap!, reset!
+ (implemented as a wrapper object)
+
Most of the standard library: map, filter,
+ reduce, str, get,
+ assoc, update, merge,
+ select-keys, etc.
+
+
+
Data structures are JavaScript types
+
+
This is the biggest difference. Squint maps are JavaScript objects
+ and vectors are JavaScript arrays — but the standard library preserves
+ Clojure-like semantics where possible.
(defm {:a1})
+(assoc!m:b2)
+;; m => {:a 1 :b 2} (mutated in place)
+
+
This applies to assoc/assoc!,
+ dissoc/dissoc!,
+ conj/conj!, etc. Use the non-bang version
+ when you want Clojure-like copy semantics. Use the bang version when
+ you need performance and know mutation is safe.
+
+
The key difference from Clojure: shallow copies don't use structural
+ sharing, so copying large collections is more expensive than in Clojure.
+ Also, nested objects still share references:
+
+
(let [m {:a {:b1}}
+ m2 (assocm:c2)]
+ (identical? (:am) (:am2)))
+;; true — nested objects are shared, not deep-copied
+
+
Equality
+
+
Squint's = implements deep equality, matching
+ Clojure's semantics — it does not simply compile to JavaScript's
+ ===:
Unlike Clojure, there's no :use or :import.
+ Macros must be loaded via :require-macros with
+ :refer — using plain :require will import
+ them as regular functions instead of expanding at compile time.
+ See the Macro Guide for details.
+
+
What's missing
+
+
Features from Clojure/ClojureScript that are not available
+ in Squint:
+
+
+
Protocols and records — use defclass for JS
+ classes instead
warn-on-lazy-reusage! — runtime detection of
+ uncached lazy sequence reuse
+
Direct npm package requires via string names
+
+
+
Also note: collections cannot be used as functions. In Clojure
+ you can write ({:a 1} :a) to look up a key. In Squint,
+ you must use (get {:a 1} :a) or (:a {:a 1})
+ (keywords as functions work).
+
+
Another difference: map keys must be strings. Since Squint maps
+ are JavaScript objects, only string keys are supported. Using a vector
+ or other non-string as a map key will silently stringify it.
+
+
Gotchas
+
+
Surprises for experienced Clojure developers:
+
+
+
seq returns the collection itself (or null),
+ not a lazy seq wrapper. Sequences are JavaScript iterators.
+
+
Shallow copy, not structural sharing. Non-bang functions
+ like assoc return shallow copies, but nested objects
+ still share references. This is less efficient than Clojure's
+ persistent data structures for large collections.
+
+
println uses console.log, not
+ *out*. There's no output stream redirection.
+
+
Truthiness matches Clojure, not JavaScript. Only
+ nil, undefined, and false
+ are falsy. Unlike raw JavaScript, 0 and ""
+ are truthy in Squint.
+
+
Lazy sequences are uncached. Iterating over a lazy sequence
+ twice will compute it twice. Squint warns about this at runtime.
+
+
require at the REPL works differently than in
+ Clojure — you can't dynamically load namespaces at runtime in the
+ same way.
Squint is a light-weight dialect that brings together the
+ expressiveness of Clojure with
+ the reach of JavaScript
+ — compiling Clojure syntax to lean, readable JS.
+
+
+
Native JS output: Compiles to plain JavaScript objects,
+ arrays, and ES modules. No custom data structures, no runtime overhead.
+
Tiny bundles: Because Squint uses built-in JS types,
+ compiled output is small and startup is fast. Ship production code
+ with minimal dependencies.
+
Clojure standard library: A re-implemented subset of the
+ ClojureScript standard library, ported to work with mutable JS
+ data structures.
+
Modern JS features: First-class support for async/await,
+ JSX, generators, ES modules, and lazy sequences backed by JS iterators.
+
+
+
Anywhere you can run JavaScript, you can run Squint.
import * as fs from"fs/promises";
+
+async function readConfig(path) {
+ const text = await fs.readFile(path, "utf-8");
+ const { host, port } = JSON.parse(text);
+ return { host, port: port ?? 3000 };
+}
+
+function handler({ method, url }) {
+ console.log(method, url);
+ return new Response("OK", { status: 200 });
+}
+
+
+
+
The JavaScript shown is simplified for clarity — the actual
+ compiler output uses a small runtime for Clojure semantics
+ (deep equality, truthiness, etc.). See exact output in the
+ playground.
+
+
+
+
Don't feel like installing it?
+
+
Try Squint right here — type an expression and press Enter:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
For a richer experience with syntax highlighting and sharing,
+ use the full playground.
If you need persistent immutable data structures and the full
+ ClojureScript standard library, check out
+ Cherry —
+ Squint's sibling project. Cherry uses the same compiler infrastructure
+ but preserves ClojureScript's immutable semantics, trading bundle size
+ for richer data structure guarantees. Cherry is more experimental and
+ not yet recommended for production use.
+
+
+
+
Community
+
+
+
GitHub —
+ source code, issues, and discussions.
+
Clojurians Slack —
+ join the #squint channel for questions and discussion.
+
Sponsor —
+ support Squint's development via GitHub Sponsors.
While Squint shields you from most JavaScript syntax, you'll encounter
+ JS concepts in error messages, library documentation, and interop code.
+ This primer covers what you need to know about the runtime your code
+ targets.
JavaScript has 7 primitive types and one complex type:
+
+
// Primitives
+42// number (64-bit float, no int type)
+"hello"// string
+true// boolean
+null// null (Squint's nil)
+undefined // undefined (absent value)
+Symbol("id") // symbol (unique identifier)
+42n// bigint (arbitrary precision integer)
+
+// Complex type
+{ a: 1 } // object (Squint's maps)
+
+
Truthiness: In raw JavaScript, 0, "",
+ null, undefined, NaN, and
+ false are all falsy. However, Squint follows
+ ClojureScript semantics: only nil (null),
+ undefined, and false are falsy.
+ 0 and "" are truthy in Squint. This means
+ (when count ...)will execute even when
+ count is 0. Be aware of this difference
+ when reading JavaScript documentation.
+
+
Numbers: JavaScript has only one number type: 64-bit floating
+ point. There's no integer type. This means (/ 10 3) gives
+ 3.3333..., not 3. Use
+ js/Math.floor for integer division.
+
+
Objects and arrays
+
+
These are the data structures that Squint maps and vectors compile to.
+
+
// Objects — Squint maps compile to these
+const person = { name: "Alice", age: 30 };
+person.name; // "Alice"
+person["age"]; // 30
+person.email = "a@b"; // mutation — adds a key
+
+// Arrays — Squint vectors compile to these
+const nums = [1, 2, 3];
+nums[0]; // 1 (zero-indexed)
+nums.length; // 3
+nums.push(4); // mutation — adds to end
+
+
Objects are compared by reference, not by value:
+ { a: 1 } === { a: 1 } is false because
+ they're two separate objects in memory.
+
+
Functions
+
+
JavaScript has several function syntaxes. Squint's defn
+ compiles to function expressions assigned to variables, and
+ fn to function expressions (or arrow functions with
+ ^:=>):
+
+
// function expression (from defn)
+var greet = function(name) {
+ return"Hello, " + name;
+};
+
+// function expression (from fn)
+const add = function(a, b) { return a + b; };
+
+// arrow with body
+const process = (x) => {
+ const y = x * 2;
+ return y + 1;
+};
+
+
Key differences from Clojure functions: JS functions can be called with
+ any number of arguments without error (extras are ignored, missing ones
+ are undefined). They also have a this context
+ which matters for method calls.
+
+
Modules
+
+
Squint's ns/:require compiles to ES modules,
+ the modern JavaScript module system:
ES modules are statically analyzable — imports and exports must be at
+ the top level, not inside conditionals. This is why Squint requires
+ all :require forms inside ns.
+
+
Promises and async
+
+
Promises are JavaScript's mechanism for asynchronous computation.
+ When you use ^:async and js-await in Squint,
+ you're working with promises.
+
+
// A promise represents a future value
+const promise = fetch("https://api.example.com/data");
+
+// async/await — what Squint compiles to
+async function getData() {
+ const response = await fetch("https://api.example.com/data");
+ const json = await response.json();
+ return json;
+}
+
+// An async function always returns a promise
+getData().then(data => console.log(data));
+
+
Key things to know:
+
+
An async function always returns a Promise
+
await can only be used inside async
+ functions (or at the top level of ES modules)
+
Unhandled promise rejections become errors — always handle them
+ with try/catch (which Squint provides via try)
+
+
+
npm and Node.js
+
+
npm is the JavaScript package manager. When you run
+ npm install some-package, it downloads the package into a
+ node_modules directory and records the dependency in
+ package.json.
+
+
Node.js is the most common server-side JavaScript runtime.
+ It provides built-in modules like fs (file system),
+ path, http, and child_process.
+ You can require these in Squint with string requires:
Other runtimes like Bun and Deno are largely compatible
+ with Node.js APIs and npm packages.
+
+
The browser and the DOM
+
+
In browsers, JavaScript can interact with the page via the DOM
+ (Document Object Model). You access it through global objects like
+ document and window:
Most modern web development uses frameworks (React, Vue, Svelte) rather
+ than direct DOM manipulation. Squint's JSX support makes it a natural fit
+ for React-based development.
+
+
JavaScript gotchas
+
+
+
null vs undefined:
+ null means "explicitly no value" (Squint's nil).
+ undefined means "not set at all" (missing object keys,
+ uninitialized variables). Both are falsy.
+
+
=== vs ==: Squint's =
+ implements deep equality for compound types (objects,
+ arrays, Sets, Maps), not JavaScript's ===. For
+ primitives it behaves like ===. Use
+ identical? if you need reference equality.
+
+
typeof null === "object": An ancient JS bug
+ that will never be fixed. Use nil? in Squint instead.
+
+
Array methods return new arrays: Methods like
+ .map(), .filter(), .slice()
+ return new arrays. Methods like .push(),
+ .sort(), .splice() mutate in place.
+ Squint's standard library wraps these, but if you call JS methods
+ directly, be aware of which mutate.
+
+
String immutability: Unlike objects and arrays, JavaScript
+ strings are immutable. All string operations return new strings.
Macros are a defining feature of Lisp languages. They let you transform
+ code at compile time, extending the language with your own syntax.
+ Squint supports defmacro for compile-time code
+ generation.
A macro is a function that runs at compile time. It receives
+ unevaluated code as data, transforms it, and returns new code that gets
+ compiled in its place.
+
+
;; A simple macro that logs before executing
+(defmacrowith-timing [label & body]
+ `(let [start# (js/Date.now)
+ result# (do ~@body)
+ elapsed# (- (js/Date.now) start#)]
+ (println ~label"took"elapsed#"ms")
+ result#))
+
+;; Usage
+(with-timing"fetch"
+ (js-await (js/fetchurl)))
+
+
When the compiler encounters (with-timing ...), it calls
+ the macro function, which returns a let form that wraps
+ the body with timing code. The returned code is then compiled normally.
+
+
Quoting and unquoting
+
+
Syntax-quote (`) is the primary tool for building code
+ in macros:
+
+
;; ` (syntax-quote) — returns code as data, with namespace resolution
+`(+12) ; => the list (+ 1 2)
+
+;; ~ (unquote) — evaluates within syntax-quote
+(let [x42]
+ `(+ ~x1)) ; => (+ 42 1)
+
+;; ~@ (unquote-splicing) — splices a list into the surrounding form
+(let [args [123]]
+ `(+ ~@args)) ; => (+ 1 2 3)
+
+;; # (auto-gensym) — generates unique symbol names
+`(let [x#1] x#) ; => (let [x__123 1] x__123)
+
+
The auto-gensym (#) suffix is important — it prevents
+ your macro's internal variables from colliding with user code. Always
+ use it for temporary bindings in macros.
+
+
Common patterns
+
+
Wrapping patterns — add behavior around existing code:
Note: Macros that generate try/catch
+ forms using syntax-quote have a known compiler bug — the
+ catch clause may not compile correctly. Use
+ try/catch directly in your code instead
+ of inside macros.
Important: Use :require-macros, not plain
+ :require. Plain :require will import the
+ macro file as a regular module instead of expanding macros at compile
+ time.
+
+
Macros can also be defined in the same file where they're used, but
+ note that the macro definition itself will be emitted into the compiled
+ output, which may cause runtime errors. Separate macro files are the
+ recommended approach.
+
+
Macros are resolved at compile time. The compiler uses
+ SCI (Small Clojure
+ Interpreter) to evaluate macro definitions, then uses them to expand
+ macro calls in the requiring file.
+
+
Note: Cross-namespace macro resolution has some known
+ limitations — qualified macro calls and aliased namespaces may not
+ resolve correctly in all cases. Keep macros simple and test
+ thoroughly.
+
+
Macros vs. functions
+
+
Use functions by default. Only reach for macros when you need to:
+
+
+
Control evaluation: Functions evaluate all arguments before
+ the call. Macros receive unevaluated code, so they can decide what
+ gets evaluated and when (e.g., short-circuiting, conditional
+ execution).
+
Generate new bindings: Only macros can introduce
+ let, def, or parameter bindings into the
+ calling code.
+
Transform syntax: If you want to provide a different
+ syntactic interface (e.g., a DSL), macros are the tool.
+
Eliminate runtime overhead: Macros expand at compile time,
+ so they can inline code that a function would need to dispatch
+ dynamically.
+
+
+
If a function can do the job, prefer it. Functions are simpler to
+ understand, test, and compose. Macros add power but also cognitive
+ overhead.
+
+
Debugging macros
+
+
When a macro doesn't produce the expected output:
+
+
+
Check the expansion. Use macroexpand or
+ macroexpand-1 to see what code your macro generates.
+
Print during compilation. Adding (println ...)
+ inside defmacro (outside the syntax-quote) will print
+ at compile time.
+
Check for missing gensyms. If you get "variable not found"
+ errors, you probably forgot a # suffix on a temporary
+ binding.
+
Check for missing unquotes. If a variable name appears
+ literally in the output instead of its value, you forgot a
+ ~.
This guide walks through migrating an existing ClojureScript project
+ to Squint. It's based on real-world porting experiences and covers
+ the common issues you'll encounter.
Not every ClojureScript project is a good candidate for porting.
+ Squint works best when:
+
+
+
Your project has heavy JS interop — this is where Squint
+ shines, since there's no clj→js conversion overhead.
+
You care about bundle size and startup time — serverless,
+ edge computing, CLI tools.
+
You don't rely on persistent data structures for correctness
+ (undo/redo, time-travel debugging).
+
You don't use protocols, multimethods, or spec.
+
+
+
Projects that are poor candidates: large apps using Reagent/
+ Re-frame (use Reagami
+ instead), anything relying on core.async, or code-sharing with
+ Clojure JVM via .cljc.
+
+
Project setup
+
+
Create a fresh npm project alongside your existing CLJS project:
Remove :import — use :require with
+ string names for all JS packages
+
Keep :require-macros for macro imports — this is
+ required in Squint (plain :require won't expand
+ macros at compile time)
+
Replace Google Closure library usage with npm packages or
+ native JS
+
+
+
Data structure changes
+
+
Most code works without changes because Squint's non-bang functions
+ (assoc, conj, etc.) return shallow copies,
+ approximating Clojure's value semantics. Note: unlike Clojure's
+ structurally-shared persistent data structures, Squint uses shallow
+ cloning — nested objects share references. Watch for these cases:
+
+
;; Remove clj->js / js->clj — not needed, everything is JS
+;; Before:
+(clj->js {:a1})
+;; After:
+{:a1} ; already a JS object
+
+;; Collections as functions — must use get
+;; Before:
+(my-map:key)
+;; After:
+(:keymy-map) ; keyword-as-function works
+(getmy-map:key) ; or explicit get
+
+;; Non-string map keys — won't work
+;; Before:
+{[12] "value"} ; CLJS supports this
+;; After: restructure to use string keys, or use js/Map
+;; (but note: Squint's get/assoc don't work on js/Map —
+;; use .get/.set methods directly)
+
+
Simplify interop
+
+
One of the biggest wins of porting: all the clj->js,
+ js->clj, and #js ceremony disappears:
clojure.spec → runtime validation with plain functions,
+ or a JS schema library (Zod, etc.)
+
clojure.test → Node's node:test or Vitest
+
+
+
Check the squint-macros
+ project for compatibility macros that ease the transition.
+
+
Test the port
+
+
After converting, test incrementally:
+
+
# Compile everything
+$ npx squint compile src/**/*.cljs
+
+# Run your entry point
+$ node out/my_app/core.mjs
+
+# Or use squint run for quick iteration
+$ npx squint run src/my_app/core.cljs
+
+
Common issues at this stage:
+
+
Import errors — check that npm packages are installed
+ and string requires are correct.
+
Missing functions — some CLJS stdlib functions may not
+ exist. Check the standard library
+ listing.
+
Lazy sequence reuse — if you iterate a lazy seq multiple
+ times, call (vec ...) to materialize it first.
+
+
+
Porting checklist
+
+
+
Remove :import from ns forms (keep
+ :require-macros — it's needed for macro expansion)
+
Replace CLJS library requires with npm packages
+
Remove all clj->js / js->clj calls
+
Replace .then chains with ^:async /
+ js-await
+
Change (my-map :key) to (:key my-map)
+ or (get my-map :key)
+
Check for non-string map keys
+
Materialize lazy sequences that are consumed multiple times
+
Replace protocols with defclass or plain functions
ClojureScript is a remarkable achievement — a full Clojure implementation
+ targeting JavaScript. But its design priorities create friction in certain
+ environments:
+
+
+
Bundle size. ClojureScript ships its own implementations of
+ persistent data structures, protocols, multimethods, and much of the
+ Clojure standard library. Even a minimal "hello world" produces a
+ non-trivial bundle. For edge computing (Cloudflare Workers), serverless
+ functions, or CLI tools, this overhead matters.
+
+
Startup time. Loading and initializing the ClojureScript runtime
+ takes time. In environments like Cloudflare Workers or AWS Lambda where
+ cold starts affect user experience, this is a real cost.
+
+
Build complexity. ClojureScript relies on Google Closure Compiler
+ for optimization and dead-code elimination. This is powerful but adds
+ build complexity and can be challenging to integrate with modern JS
+ tooling (Vite, esbuild, etc.).
+
+
Interop friction. ClojureScript's persistent data structures
+ require conversion when crossing the JS boundary. You constantly reach
+ for clj->js and js->clj, or carefully manage
+ which data you keep as JS objects vs. CLJS maps.
+
+
+
Squint's approach
+
+
Squint takes a different path: use only native JavaScript data
+ structures. This single decision cascades through the entire design:
+
+
+
Maps are JavaScript objects. Vectors are JavaScript arrays.
+ Sets are JavaScript Set objects.
+
No runtime library for data structures — the output is plain JS.
+
No clj->js / js->clj — there's nothing
+ to convert because it's all JS already.
+
Standard library functions like map, filter,
+ assoc, and get are re-implemented to work
+ with native JS types.
+
Output is clean, readable ES modules that work with any JS toolchain.
+
+
+
The result: Squint gives you Clojure's expressive syntax — its
+ destructuring, its threading macros, its let bindings,
+ its namespaces — on top of JavaScript's data model.
+
+
The trade-offs
+
+
This is not a free lunch. By choosing native JS data structures, Squint
+ gives up some of Clojure's core guarantees:
+
+
+
Shallow copies, not structural sharing. Non-bang functions
+ like assoc and conj return shallow copies
+ (preserving Clojure's copy semantics), but without structural sharing
+ this is less efficient for large collections than Clojure's persistent
+ data structures. Bang variants (assoc!,
+ conj!) mutate in place for performance-critical code.
+
+
Mutable underlying types. Since maps are JavaScript objects
+ and vectors are arrays, they can be mutated by JS code that
+ doesn't go through Squint's standard library. Direct JS interop
+ can bypass the copy semantics.
+
+
String-only map keys. JavaScript objects only support string
+ keys. Non-string keys (vectors, numbers) get silently stringified.
+
+
Subset of ClojureScript. Not all ClojureScript features are
+ available. Protocols, multimethods, and some advanced macro features
+ are absent or limited.
+
+
+
These are real trade-offs. Whether they matter depends entirely on your
+ use case.
+
+
When to use Squint
+
+
+
Edge/serverless computing. Cloudflare Workers, AWS Lambda,
+ Deno Deploy — environments where bundle size and cold-start time
+ directly impact cost and user experience.
+
Scripts and CLI tools. Node.js scripts, build tools, GitHub
+ Actions — where you want Clojure's expressiveness but don't need a
+ full application runtime.
+
JS-heavy projects. When you're already deep in the JS
+ ecosystem and want a better syntax without switching data models.
+
Small to medium applications. Where the simplicity of
+ "it's just JavaScript" outweighs the guarantees of persistent
+ data structures.
+
+
+
When not to use Squint
+
+
+
Large applications relying on immutability. If your architecture
+ depends on persistent data structures for correctness (e.g., undo/redo,
+ time-travel debugging, concurrent updates), use ClojureScript or
+ Cherry.
+
When you need the full Clojure ecosystem. If you need protocols,
+ multimethods, spec, core.async, or the full ClojureScript standard
+ library, Squint is not the right tool.
+
When you can afford the bundle size. For traditional web
+ applications served from a CDN, ClojureScript's bundle size is rarely
+ the bottleneck. Use the tool that gives you the strongest
+ guarantees.
+
+
+
What about Cherry?
+
+
Cherry is Squint's
+ sibling project, built on the same compiler infrastructure. Where Squint
+ chooses native JS types, Cherry preserves ClojureScript's immutable
+ persistent data structures.
+
+
Think of them as two points on a spectrum:
+
+
+
ClojureScript — full language, Google Closure, largest bundles,
+ strongest guarantees
+
Cherry — ES module output, persistent data structures, medium
+ bundles, strong guarantees (experimental)
Cherry is currently experimental and not recommended for production use.
+ If you need immutable data and are willing to accept a larger bundle,
+ keep an eye on its development.
Squint inherits most of its style conventions from Clojure. This guide
+ covers the conventions specific to Squint and areas where JavaScript
+ interop creates new questions.
Use kebab-case for functions and variables:
+ fetch-data, user-name
+
Use PascalCase for React components and classes:
+ App, UserProfile
+
Use SCREAMING_SNAKE_CASE for constants:
+ MAX-RETRIES (though kebab-case is also fine)
+
Predicates end with ?:
+ valid?, empty?
+
Side-effecting functions end with !:
+ swap!, reset!
+
+
+
Squint automatically converts kebab-case to camelCase in the compiled
+ output, so fetch-data becomes fetchData in JS.
+ This means you can use idiomatic Clojure names and get idiomatic JS
+ output.
Group requires: external packages first, then project namespaces.
+ Prefer :as over :refer for external packages
+ to make it clear where functions come from.
+
+
Interop style
+
+
When to use Squint's standard library vs. direct JS interop:
+
+
;; Prefer: Squint stdlib when it exists
+(mapinc [123])
+(assocm:keyvalue)
+(filterodd?nums)
+
+;; Also fine: JS methods for simple operations
+(.toUpperCases)
+(.slicearr05)
+(.startsWithpath"/")
+
+;; Prefer: JS methods when there's no Squint equivalent
+(.padStarts10"0")
+(js/Object.entriesm)
+(js/JSON.stringifydata)
+
+
Use Squint's stdlib when it exists — it's more idiomatic and consistent.
+ Reach for JS methods when Squint doesn't provide an equivalent, or when
+ the JS method is simpler for the task at hand.
Mark functions as ^:async only when they need to
+ js-await. Don't make everything async "just in case" —
+ it adds overhead and complicates the return type.
+
+
Working with mutability
+
+
Since Squint's data structures are mutable, adopt defensive habits:
+
+
+
Don't share mutable state. If a function needs its own copy
+ of data, clone it explicitly at the boundary.
+
Prefer pure functions. Even though mutation is possible,
+ functions that take input and return output without side effects are
+ still easier to reason about.
+
Use atom for explicit state. When you need
+ mutable state, atom makes it visible and controlled.
+
Know the difference between bang and non-bang.
+ assoc returns a shallow copy, assoc!
+ mutates in place. Use bang variants only when you're sure no other
+ code holds a reference to the collection.
A note on JavaScript examples: Throughout this tutorial,
+ JavaScript output is shown in simplified form to aid
+ understanding. The actual compiler output uses a small runtime
+ library (squint_core) for Clojure semantics and may
+ differ in variable naming and code structure. Use the
+ playground
+ to see exact compiler output.
+
+
+
+
Hello World
+
+
Create a file called hello.cljs:
+
+
(nshello)
+
+(println"Hello, world!")
+
+
Run it with:
+
+
$ npx squint run hello.cljs
+Hello, world!
+
+
The ns form declares the namespace — it's how Squint knows
+ what module you're defining. Every file should start with one.
+
+
+
+
Values and types
+
+
Squint compiles to JavaScript, so all values are JavaScript values:
+
+
42; number
+3.14; number (JS has no int/float distinction)
+"hello"; string
+true; boolean
+false; boolean
+nil; compiles to null
+:foo; keyword — compiles to the string "foo"
+foo; symbol — a variable reference
+
+
Keywords like :foo are a Clojure concept. In Squint, they
+ compile to plain JavaScript strings. This means :foo and
+ "foo" are interchangeable as map keys.
Bindings are sequential — later bindings can reference earlier ones.
+ The let form compiles directly to const
+ declarations in JavaScript.
+
+
+
+
Data structures
+
+
Important: Squint's data structures are JavaScript objects and
+ arrays under the hood. The standard library functions like
+ assoc and conj return shallow copies
+ (not persistent data structures). Use bang variants
+ (assoc!, conj!) when you want in-place
+ mutation. See the Rationale for why.
The standard library functions (map, filter,
+ reduce, assoc, get, etc.) work on
+ these structures just as you'd expect from Clojure, but remember they
+ operate on mutable JavaScript types under the hood.
+
+
+
+
Destructuring
+
+
Squint supports Clojure's full destructuring syntax in let,
+ fn, and defn:
+
+
Sequential destructuring (arrays):
+
+
(let [[abc] [123]]
+ (printlnabc))
+;; 1 2 3
+
+
Associative destructuring (objects):
+
+
(defngreet-user [{:keys [namerole]}]
+ (str"Hello "name", you are a "role))
+
+(greet-user {:name"Bob":role"admin"})
+;; "Hello Bob, you are a admin"
+
+
Under the hood, the compiled output uses squint_core.get()
+ to extract keys from the object — this preserves Clojure's keyword
+ lookup semantics rather than using JavaScript's native destructuring:
+
+
// Actual compiled output (simplified)
+var greet_user = function(p) {
+ const name = squint_core.get(p, "name");
+ const role = squint_core.get(p, "role");
+ return `Hello ${name}, you are a ${role}`;
+};
+
+
+
+
Control flow
+
+
;; if — returns a value (it's an expression)
+(if (>x0)
+ "positive"
+ "non-positive")
+
+;; when — like if but no else branch
+(when (valid?input)
+ (processinput))
+
+;; cond — multi-branch conditional
+(cond
+ (<n0) "negative"
+ (=n0) "zero"
+ :else"positive")
+
+;; case — constant matching
+(casedirection
+ "north" [01]
+ "south" [0-1]
+ "east" [10]
+ "west" [-10])
+
+
Looping uses loop/recur (compiles to a
+ while loop) or doseq/dotimes:
The #jsx tag converts hiccup-style vectors into JSX
+ that works directly with React and other JSX-consuming libraries.
+
+
+
+
This covers the basics. For a complete reference of all built-in
+ forms, see the API Listing. For Clojure developers,
+ the Squint from Clojure guide covers
+ important differences.