Type generation: varlatch types and the Typed Accessor
varlatch types turns a Contract Revision into one TypeScript or Python
module. The module declares a type for every Contract item and carries the
Typed Accessor, a small runtime that reads the environment, converts each
value with the revision’s Contract Semantics, and checks it. The module is
self-contained: your application imports it and needs nothing else from
Varlatch. This page describes the TypeScript module first; Python
lists what differs for Python.
varlatch types --out <file.ts|file.py> [--revision <id>] [--check]Generating the module
Section titled “Generating the module”varlatch types reads the active Contract Revision of the project named in
varlatch.toml, or the revision given with --revision. It makes that one
request and nothing else. It needs contract.read on the project, and it
never fetches a value. No environment needs to be selected: the output is the
same for every environment.
--outnames the file to write, and its extension selects the language:.ts,.mts, or.ctsfor TypeScript, and.pyfor Python.- Only on change. The file is written only when its content changes. An unchanged file keeps its modification time, so build tools do not rebuild.
- Never through a symbolic link. If the output path is a symbolic link,
varlatch typesrefuses and writes nothing. New content goes to a new file in the same directory, which then replaces the old one, and the old file’s permissions are kept. No shared temporary directory is used. --checkgenerates the module in memory and compares it byte for byte with the file. It writes nothing, and exits 1 when the file is stale or missing. Run it in CI to catch a Contract change that nobody regenerated.--revision <id>generates from a stored revision that need not be active. It needs a server running Varlatch 0.11.0 or later.- Semantics versions. Types need conversion, which Contract Semantics
version 1 does not define. A revision at version 1 is refused; push the
Contract again with
varlatch contract push --semantics latestand activate it. A version this CLI does not implement is refused too; upgrade the CLI. See Contract Semantics.
varlatch types runs only when you run it. varlatch run never regenerates
types.
The file’s header records the Contract Revision ID, its content hash, the
semantics version, and the CLI release that generated it. The output depends
only on the revision and the CLI release. A file that differs only in those
Generator lines counts as current: --check passes on a file another release
generated as long as that release would produce the same module, so CI and
developers can run different releases.
Updating the accessor. The Typed Accessor is part of the generated file,
not a package your application installs, so a fix to it reaches your
application only when you regenerate. After upgrading the CLI, run
varlatch types again and commit the new file. It rewrites the file only when
the new release generates a different module, and varlatch types --check
fails until you do; a file that differs only in its Generator lines is left as
it is.
What the module contains
Section titled “What the module contains”| Contract type | TypeScript type |
|---|---|
string, email |
string |
url |
string, validated as a URL |
number |
number |
integer |
number, documented as a whole number |
boolean |
boolean |
enum |
a union of the listed values |
Confighas one property per Contract item. A property is required only when the Contract requires the item in every environment and gives it no default. Every other item is optional, because it can be absent: a default is applied only in the cases below, and never replaces a value the server withheld.- Conditions are kept. An item required only in some environments, by tier or by environment, is optional in the type, and its condition is kept in the module as data. The accessor checks it whenever a run context names the environment.
- Documentation. Each property’s comment carries the item’s description,
its example, its requiredness, a
@defaulttag naming a default, and a@sensitivetag for Secrets. Contract text is cleaned before it reaches a comment: line breaks are normalized, control and bidirectional formatting characters are removed, and*/is neutralized, so no description can end the comment early or hide text in it. PublicConfighas only the non-sensitive items. It is empty for a Contract of Secrets only.config,loadConfig(),ConfigError, andgeneratedFrom(the revision the file was generated from) are the runtime exports.
The file starts with // @ts-nocheck and /* eslint-disable */. The runtime
at its end is bundled JavaScript that your compiler and linter settings
should not check. The exported types are exact all the same, and the file
compiles under strict settings as an ES module or as CommonJS, without Node
or DOM type definitions. The runtime is licensed under Apache-2.0, and the
file carries its notice.
Commit the file like any other source. It contains Contract metadata (item names, descriptions, examples, defaults, enum values, and the environment IDs in conditions) and never a value. Contract defaults and examples are metadata that anyone with metadata access can read; never put a secret in them.
Reading configuration
Section titled “Reading configuration”import { config } from "./config/varlatch.js";
server.listen(config.PORT); // a numberconfigreads and validates every item the first time it is used, with the default options.loadConfig(options)reads and validates now. It returnsconfig, and a report:defaulted,notEvaluated,context, andwarnings.
The accessor follows these rules:
- It only reads. Values come from
process.env, or from theenvoption. The accessor never writes toprocess.envand adds no variable to it, so every value there stays the string Varlatch delivered. Converted values exist only in the accessor’s object. - One error, never a value. Every item is validated on first use. Any
problem throws one
ConfigErrorwhoseissueslist each item by name and reason, never by value. After a failed first use, every later use ofconfigthrows the same error, so an item that failed never reads asundefined. - Read-only.
Object.keys,in,JSON.stringify, and spreading see the items. Any assignment,delete, orObject.definePropertythrows aTypeError, in strict and sloppy code alike. - Presence comes from
process.env. An item that is present is always read and validated, whatever the run context says about how it got there. For example, a Secret the server withheld that the shell supplied throughvarlatch run --strict --allow-inherited API_KEYis returned like any other value. - Read once.
configreadsprocess.envon first use. A later change toprocess.envis not reflected; callloadConfig()again to read it.
Defaults
Section titled “Defaults”The accessor fills an absent item with its Contract default only when you
ask for it with applyDefaults: true, and reports each filled item in
defaulted.
| The application was started by | Defaults applied by the accessor |
|---|---|
varlatch run --strict |
None. Strict startup already applied defaults where they belong, and applyDefaults has no effect. An absent item is an error only if it is required in this environment. |
varlatch run --export-context |
With applyDefaults: true only, and never for an item the server withheld. |
| anything else, with no run context | With applyDefaults: true only. The accessor cannot tell why an item is absent. |
The run context
Section titled “The run context”When VARLATCH_RUN_CONTEXT is set (see Strict
startup), the accessor checks
requiredness for the environment that run used, and knows which items the
server withheld.
- An unknown context version, malformed JSON, or a malformed field throws.
- A semantics version other than the module’s throws.
- A different content hash means the types are stale. The accessor warns
(a
VarlatchWarningthroughprocess.emitWarning), or throws withstaleTypes: "throw". It compares content hashes, not revision IDs, so an identical Contract activated again as a new revision is not stale. - An item the context records as delivered but that is absent from
process.env, because the application removed it, is treated as absent. - Without a context, types and items required in every environment are
checked, and an absent item that is required only in some environments is
listed in
notEvaluated.requireContext: truemakes a missing context an error.
Options
Section titled “Options”| Option | Default | Effect |
|---|---|---|
env |
process.env |
Where values are read. It is never written to. |
applyDefaults |
false |
Fill absent items with Contract defaults, as described above. |
staleTypes |
"warn" |
"throw" makes a content-hash mismatch an error. |
requireContext |
false |
Throw when VARLATCH_RUN_CONTEXT is not set. |
onWarning |
process.emitWarning |
Receives each warning. |
Python
Section titled “Python”varlatch types --out app/varlatch_config.pyfrom app.varlatch_config import config
serve(port=config.PORT) # an intThe Python module needs Python 3.10 or later and the standard library only.
Python 3.10 is supported until the first Varlatch release after 31 October
2026, when its upstream support has ended; from then on, the module needs
Python 3.11 or later. Generation, --check, --revision, the header, and
the refusals are the same as for TypeScript. The accessor follows the same
rules as the TypeScript one (it only reads, reports every problem in one
error without a value, takes presence from the environment, applies defaults
only as described in Defaults, and reads the run
context), with these differences.
| Contract type | Annotation | Value |
|---|---|---|
string, email, url |
str |
the string |
number |
float |
an int when the value has no decimal point, such as 3000; a float otherwise, such as 0.5 or 3000.0 |
integer |
int |
an int; the value is always a whole number |
boolean |
bool |
True or False |
enum |
typing.Literal of the listed values |
the string |
- Numbers. A whole number is an
int, so counts, sizes, and ports work with slicing andrange(). Which numbers are valid is exactly as for TypeScript, including the bound of 9007199254740991.-0is the int0. The annotation isfloat, which also accepts anint, so a type checker may ask you to convert withint(...)where it needs an int. Declare the itemintegerinstead (Contract Semantics version 3) and it is annotatedint, with fractions rejected. - Absent items are
None. An optional item is annotatedX | None. An empty string is a value, neverNone. Configis a frozen dataclass: assigning to or deleting an attribute raisesdataclasses.FrozenInstanceError. (Python cannot stop code that callsobject.__setattr__on purpose.)config.public()returns aPublicConfigwith the non-sensitive items only.load_config()takes keyword arguments:env(a mapping, defaultos.environ),apply_defaults,stale_types("warn"or"raise"),require_context, andon_warning. It returns aLoadResultwithconfig,defaulted,not_evaluated,context, andwarnings.config, a module attribute, is read and checked the first time it is used, then kept.from app.varlatch_config import configuses it, so the import itself reads and checks the configuration. If the configuration is invalid, every use raises aConfigErrorwith the same issues.from ... import *does not load it.- Errors and warnings.
ConfigError.issuesis a tuple ofConfigIssuenamed tuples, each a name and a reason. Stale types are reported throughwarnings.warnwith the categoryVarlatchWarning, unless you passon_warning. - URLs. A URL is validated as the server validates it. A URL whose host
name is internationalized, such as
https://bücher.example, needs the optionalada-urlpackage (the URL parser Node uses) to be checked. Without it, such a URL is reported as “is an internationalized URL; install the ada-url package to validate it”, never accepted unchecked. URLs with ASCII host names need nothing extra. - Text only. On Linux and macOS, a value in the environment that is not valid UTF-8 is reported as an issue rather than read with replacement characters.
- Secrets and error reports. Sensitive items are left out of
repr(config), so logging the object or printing it in a traceback shows no Secret;dataclasses.asdict()still returns every value. The accessor keeps values out of its own messages, and out of its own stack frames when it raises. Your code’s frames are yours: a variable that holdsconfigor a value read from it holds a Secret. If you use an error tracker that records local variables, turn that off or scrub Secrets there; hiding items fromrepr()is not a guarantee against a tracker that reads attributes directly. - Checks on the file. The file starts with
# ruff: noqa,# mypy: ignore-errors, and# fmt: off, because the runtime at its end is not your code. Your own code is still checked against its types.
Exporting the run context from a default run
Section titled “Exporting the run context from a default run”varlatch run --export-context -- <command> [args...]--export-context makes an ordinary varlatch run also give the command
VARLATCH_RUN_CONTEXT, with "mode": "exported". The run is otherwise
unchanged: the same values, the same precedence (a delivered value replaces
your shell’s, anything else is inherited), no defaults, and no failure
because of the Contract. The context records, for each Contract item, what
the server did and how the item reached the command, with names and
identifiers only.
- It fetches the Contract Revision that the configuration response names,
before any Secret is disclosed. It needs
contract.read, an active Contract, and a server running Varlatch 0.11.0 or later; otherwise the run starts nothing. - The context always describes the values the command receives. A default
run makes two requests, the configuration and then the disclosure of its
Secrets, and each is answered from its own database snapshot. With
--export-contextthe run compares thestateDigestof both responses, so it knows they saw the same environment, Contract Revision, and value versions. When something changed in between (a value written, rotated, or deleted, or a Contract activated), it makes both requests again, up to three times in all. Each time is another disclosure, and is audited as one. If the two responses never agree, the run starts nothing and exits with status 1, naming no value. A Grant change in between cannot make the context wrong: each item’s status comes from the response that delivered or withheld it. - An item left out with
--omitis recorded withdelivery: "absent", even when your shell sets it (the run removes that copy). An omitted Secret was never asked for, so it is recorded as stored (server: "delivered"), not as withheld; the accessor then reports an omitted item that is required in this environment as absent. - A context larger than 64 KiB fails the run; it is never truncated.
- It applies only to default runs. A
--strictrun always gives the command its run context.
Without --export-context, a default run removes a VARLATCH_RUN_CONTEXT
inherited from an outer run, so the command never sees a context that
describes another environment.
Limits
Section titled “Limits”- The accessor is a correctness check, not a confidentiality control. A
process can read everything in its environment, and logging
configprints its values, Secrets included, as loggingprocess.envwould. - Without a run context the accessor cannot tell a withheld item from one that is not stored, and does not check conditions that depend on the environment.
- Types are generated from server revisions only, in TypeScript and Python.

