:typed-check-cache, default :on):
no finding counts as clean, findings print the same WARNING lines (:error fails the load
with them), a skip prints the same skipped line. The fingerprint = the form, its schema, the
schemas of the functions it calls, Typed Clojure's and defn-typed's versions; outcomes are kept
on disk in .defn-typed/typed-check-outcomes.txt under the working directory (gitignore
.defn-typed/). A warm load of an unchanged project runs no check and does not load Typed
Clojure; a callee's changed schema re-checks its callers. -Ddefn-typed.typed-check-cache=off
(or :typed-check-cache :off in defn-typed.edn) reads and writes nothing.examples/claude-code emits types with defn-typed.kondo/emit!..cache (0.3.2 carried one under clj-kondo.exports/): removed from git, ignored, and build.clj drops any .cache dir before jarring.create-class
render fn re-destructuring the props) is read; unused binding stays for a row named nowhere.tests macro registers its cases in dev builds only (:dev in clj, goog.DEBUG in
cljs), as defmeta does.defmeta and its own definition is reported as a forward call (its
declaration is the defmeta's).:literal-check; the compiled output is unchanged.Breaking
-Ddefn-typed.dev=true (or :dev true in defn-typed.edn) a Clojure JVM runs nothing
at load: check-ns returns [], deftests! defines 0 tests, defn-typed.core/instrument!
returns nil, and a bad real-map call goes unchecked, with no message. 0.3.1's dev alias
(-Ddefn-typed.inline=false alone) therefore checks nothing on 0.3.2. Upgrade: add
-Ddefn-typed.dev=true to the dev, test and REPL aliases. ClojureScript keeps goog.DEBUG.^{:as row}, ^{:closed true}) is a compile error; data the body
needs whole is a row ({:row :map}).Added
:dev setting (-Ddefn-typed.dev=true), the Clojure analogue of goog.DEBUG, off by default:
a release JVM loading a namespace only defines the functions — no in/out cases, no defmeta
registrations, no unknown-key check on real-map calls, no instrumentation upkeep, no
:stale-callers signatures or watches, no Typed Clojure check. ClojureScript's unknown-key
check moves under goog.DEBUG (a release build has none).(defn-typed first-of [T] {:xs [:sequential T]} -> [:maybe T] …); Typed Clojure gets
(t/All [T] …), malli and clj-kondo read :any. Loading defn-typed.typed-clojure types
[:tuple …] as t/HVec.defnt = defn-typed under a short name (same expansion, hooks and checks).:dev, cljs dev builds): :inout-check :warn (default) prints one line per failing case naming the differing key paths, or the
exception a case threw; :error fails the load; :off runs nothing. assert-cases (and
deftests!) carries the same line.:dev on, Typed Clojure on the classpath): :typed-check :warn|:error|:off, up to 2 s per definition. Findings lead with input of <f>: or
output of <f>:, and check-form! returns :kind (:input, :output, nil).:report-success: one line per namespace whose defn-typed checks all passed.:stale-callers (clj, :dev on): a redefinition with another signature names the namespaces
whose literal calls compiled against the old one (:warn, default), reloads them (:reload,
opt-in) or does nothing (:off); a row typed by a schema var (:order Order) follows a change
of that var.:unknown-keys :warn|:error|:off: a real map holding keys beyond the rows, in dev builds (at
most 1000 reported pairs per process). :literal-check takes :off. :literal-check,
:unknown-keys, :typed-check and :inout-check can be set per function in its defmeta.:sequential / :vector / :maybe rows: optional in <name>-props, filled at
any depth; literal items are checked by index.defn-typed.kondo/emit! {:src-dirs [...] :out path} (CLI clojure -M -m defn-typed.kondo --src-dir src --out <path>) writes the clj-kondo types of every defn-typed / defnt and every defn
with :malli/schema under the dirs, read from the source (.clj, .cljc, .cljs) without
loading it; output canonical, written only on change. defn-typed.core/form-parts = the macro's
reading of a form, public.defn-typed.core/instrument! (malli's instrument! options): a rejection's message reads
defn-typed: <fn> — invalid :<key>: expected <schema>, got <value> (malli's ex-data unchanged;
a :report of yours wins), and a redefinition re-instruments with the same options. A no-op with
:dev off.require :reload included) stays instrumented.^:private, or :private true in its defmeta) is instrumented
in dev like a public one; its <name>--positional, <name>-props (and :malli-in-prod vars)
are private too.declared above it)
compiles as a map call and is reported by the definition under :literal-check:
defn-typed: <file>:<line> calls <fn> before its definition — … (:error fails, :off silent).:type, one of :defn-typed.core/invalid-form,
invalid-schema, invalid-cases, setting, literal-call, forward-call, unknown-keys,
inout-failure, typed-check.shadow-cljs watch prints WARNING defn-typed <file>:<line>: … for a wrong literal call (:referred, qualified, aliased or inside the defining namespace) and the call stays the map call; release builds are unchanged. {:literal-check :off} keeps a dev build without the call-site macro.def and both defns the macro defines, so a direct name--positional
call resolves and its arity is checked; the linted defn carries the defmeta's :doc
(clojure-lsp hover); a row the body never reads is an unused binding; the exported config keeps
clojure-lsp from reporting <name>-props and <name>--positional as unused public vars.provided, so cljdoc analyzes defn-typed.typed-clojure.Changed
(name--positional v …) with no
let; a value that is a call keeps the let (evaluation order).{:pre … :post …} holds wherever the body lands (also a body that recurs to the
function, or a table of more than 20 rows); :post beside such a recur is a compile error, as
in fn.watch compile only what changed.Fixed
Docs and tests
defnt, the
:dev flag, an "Escape hatch" section, an "Errors" section; "Editor types (clj-kondo)" replaces
the manual malli.clj-kondo/emit! / :node-script recipe.defmeta (test/defn_typed/api_cases.cljc, test-typed/defn_typed/typed_api_cases.clj).defn-typed arglists show the real row names, and generated in/out test names use <ns>--<fn>-inout.-Ddefn-typed.inline=false turns it off); inlining while malli.instrument is loaded prints one stderr warning.:referred literal calls to positional calls.defn-typed.edn project configuration supports :literal-check :warn|:error and :inline, looked up from the working directory up; property settings take precedence over the file, which takes precedence over defaults.^:typed.clojure/ignore; optional defn-typed.typed-clojure provides install! and check-form!, while the library itself does not depend on Typed Clojure.:malli-in-prod true (or {:sample 0.01 :redact #{:phone}}) in a
function's defmeta makes malli check that function's input map and result at run time in
every build, once the app requires defn-typed.malli-in-prod (new namespace). Without that
require, the function runs unchecked and prints one line. The check never changes the result
and never throws into the caller; validators are compiled once per function, and a successful
check allocates nothing. Violations reach defn-typed.malli-in-prod/on-malli-violation! off the
call path (clj: one background thread, cljs: a 0 ms timeout) as
{:fn :direction :value :errors :schema :stack :at :repeats}: :redact keys are removed at any
depth, there is one event per function and failing paths per 60 s (the rest counted in
:repeats), and a throwing handler is caught. defn-typed.core still never loads malli: a cljs
release without the require has no malli code. The switch never rewrites an opted-in call to the
positional call.<name>-props as a plain malli schema for
forms and API input (clj and cljs :advanced).recur written by a macro in the body is found (the body is macroexpanded first), so it keeps
the map path instead of throwing ClassCastException (rv3 N2).^{:as a} with a row :a is the finding :a and ^{:as a} both bind a, the same
as the macro (rv3 N3).1.0 fits :int, [1 2] fits
[:vector :double], and a double schema's props still apply (rv3 M2 class).:undeclared-var warning.recur targets the function keeps it in the map fn, so recur takes the map as in
0.1.4 (1 row: no ClassCastException; 2+ rows: compiles).:double row in the compile-time check (cljs numbers are
doubles), so (scale {:x 1}) neither warns nor falls back to the map call.^{:closed true}); a key that is not a keyword
literal ({k 100}) skips the key checks and the rewrite.:advanced) builds only; dev builds keep the
shadow-cljs cache (before, every namespace defining a defn-typed recompiled on every build)
and compile plain calls.(defmeta 5 …), (defn-typed), an odd input map and two rows binding
one local are a finding naming the problem (the file's other findings stay) and a compile error.{:optional true} in
<name>-props, so an instrumented call may omit it.:referred call from another namespace gets neither the literal check nor the
rewrite.<key> <value> — <reason>: a part inside the value leads with its
path (at 1: should be a keyword); without a malli message, does not match <schema>.:or of the function's destructuring, taken
from the rows when the macro expands. with-defaults still runs for ^{:as row} functions,
and row-value (new) for a row whose defaults only the evaluated schema shows (a symbol as its
type, a [:map …] row with defaults inside). Results are unchanged: an explicit nil stays nil,
a default written as a call is evaluated once, at the def.<name>--positional (the rows as parameters, entry order); <name> destructures
and calls it.defn-typed.inline=true at compile time;
cljs: :optimizations :advanced), a call whose argument is a map literal of known keys holding
every required row compiles to the positional call, values evaluated in the literal's order.
clj covers every direct call (:inline); cljs covers calls through an alias, a qualified name or
inside the defining namespace (a macro of the same name), not a :referred call from another
namespace.WARNING defn-typed <file>:<line>: (f …) <findings> to
stderr and the call compiles to the map call.expand-call (the call-site expander), row-value.(defn-typed f {…} ->) with nothing after -> is a compile error naming the function ("no output
schema after ->"); the clj-kondo hook reports it too (before, the macro compiled it with a nil
output schema and the hook stayed silent).malli.clj-kondo/emit!, cljs via malli.clj-kondo/print-cljs!), and what is checked statically,
at runtime and in a release.defn-typed without -> reports "expected ->" and the rest of the file still lints
(before, clj-kondo printed "Can't parse " and dropped every other finding of that file).io.github.hyperfocusdisordered/defn-typed); library code unchanged since 0.1.1.build.clj (clojure -T:build jar | deploy), GitHub Actions CI (clj, cljs, clj-kondo).:default in the row type's own props, type first: :qty [:int {:min 1 :default 1}]
(malli's schema default). defn-typed marks such a row {:optional true} itself
(defaults-optional, nested maps included); with-defaults reads the type's props.:default in a row's entry props (key [{:default v} schema]) = compile error and clj-kondo
error naming the fix; entry props stay for {:optional true} without a default.defaulted-required-keys, defaulted-key-rule and malli-reasons' defaulted-key line;
malli-fns no longer returns :form. Added: schema-props, entry-default-paths,
entry-default-rule, defaults-optional.defn-typed + defmeta, extracted from a production Clojure/ClojureScript app.check-var, check-vars, check-ns, case-vars, registered-vars,
undefined-metas, forget-ns!, deftests!, test-ns!, test-var!, malli-reasons,
malli-fns, *trace-cases*, with-defaults; legacy tests.resources/clj-kondo.exports/io.github.hyperfocusdisordered/defn-typed/.Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |