Skip to content

Configuration

Typra is configured from the @typra/emitter options in tspconfig.yaml.

emit:
- "@typra/emitter"
options:
"@typra/emitter":
emitter-output-dir: "{cwd}/generated"
root-object: "Typra.Fixtures.FixtureRoot"
deterministic-output: true
emit-targets:
- type: TypeScript
output-dir: "generated/typescript"
test-dir: "generated/typescript/tests"
import-path: "../index"
outputs:
- kind: models
- kind: native-serialization
provider: zod
- type: Java
output-dir: "generated/java"
test-dir: "generated/java/tests"
package-name: "typra.fixtures"
- type: Python
output-dir: "generated/python/typra/fixtures"
test-dir: "generated/python/tests"
import-path: "typra.fixtures"
outputs:
- kind: server
provider: fastapi
Option group Options
Root selection root-object, root-namespace, root-alias, additional-roots, omit-models
Review and safety deterministic-output, protected-paths, hydration-zones, allow-unsupported-typespec-version
Output shape emit-targets, namespace-output

root-object is the required anchor for generation. Use a fully qualified model name.

root-namespace is the semantic contract root. When set, Typra resolves models, callables, vectors, and transports from that TypeSpec namespace and preserves nested namespaces under it in the shared IR. root-alias is a naming override for generated root-owned type names; it does not replace target package options.

schema-output-dir is accepted but reserved for future manifest-based cleanup of omitted models. It does not change generated output today.

Typra separates TypeSpec contract identity from target packaging:

Concept Example Purpose
Semantic root root-namespace: "Typra.Prompty" TypeSpec namespace Typra treats as the durable contract root.
Semantic alias root-alias: "Prompty" Optional generated type-name alias for the semantic root name.
Target namespace/package namespace, package-name, import-path Runtime packaging and import ergonomics for a specific target.

Nested TypeSpec namespaces below root-namespace remain part of the lowered model/callable/transport identity. Target renderers then project the semantic namespace through runtime-specific rules. namespace-output defaults to structural, which means targets with module/folder support place nested namespaces in nested output folders. Set namespace-output: flat globally or on a target to keep a flat generated file layout while preserving semantic namespace metadata.

Target Default projection
TypeScript Uses the TypeSpec namespace as target metadata, trimming a trailing .Core; nested namespaces emit nested ESM folders and generated tests default to ../src/index.
Python Lowercase dotted package/import path, overrideable with package-name or import-path; nested namespaces emit nested packages.
C# Emits a single flat namespace — the configured namespace, otherwise the semantic-root namespace — for every type; nested TypeSpec namespaces change only the output folder layout, not the emitted namespace declaration (an IDE0130 suppression is emitted so the folder-vs-namespace analyzer stays quiet).
Java Lowercase dotted package, sanitized for Java package syntax; package-name wins over namespace. The package is a single flat value — nested namespaces do not add package segments.
Go Flat lowercase package name because Go has one package per directory; package-name or import-path can override ergonomics. Nested namespaces do not create subpackages.
Rust Root crate import path defaults to crate; nested namespaces emit nested Rust modules.
Swift Root namespace becomes a PascalCase module name, overrideable with package-name; nested namespaces emit nested source folders but remain in the one flat module (Swift has no per-directory namespaces).

The rule of thumb is: TypeSpec namespaces express contract ownership, while target options express runtime packaging. Prefer target overrides when a language ecosystem needs a different package name, not when changing the contract identity.

Two projection details are worth calling out explicitly:

  • Declaration identifiers stay flat. The emitted C# namespace and the Go, Java, and Swift-module identifiers are a single flat value. Nested TypeSpec namespaces only influence the folder / module layout for targets that nest (TypeScript, Python, Rust, Swift folders, and C# folders); they never append segments to the declared namespace/package. Targets that nest also emit flat re-export barrels (TypeScript index.ts, Python __init__.py, Rust pub use ...::*) so the public surface stays reachable from the package root.
  • Operations emit at the package root. TypeSpec-native interfaces / operations are always generated at the target root regardless of the namespace they are declared in; only models follow the namespace-derived folder layout.
  • The declared namespace — not the source folder — drives module layout. For targets that nest, a type’s output sub-path is derived from its nested TypeSpec namespace relative to the semantic root (e.g. App.Contracts.Core under root App → contracts/core/), and this is authoritative: the source .tsp file may live anywhere. To get multi-level nesting (e.g. model/contracts/<domain>/<Type>), declare the nested namespace — do not rely on nesting source folders under schema/model/. A source subfolder only contributes one grouping level and only for types whose namespace is flat at the semantic root; when a nested namespace is present the folder is ignored, so folder layout and namespace can never disagree or produce doubled path segments.
  • Namespace nesting applies to every discovered model. Typra emits both the types reachable from root-object and any additional models found by scanning root-namespace (types declared in that namespace tree but never referenced from the root object). Both sets are projected the same way, so a model in a nested namespace nests by that namespace whether or not it is reachable from root-object — you do not need to reference a type from the root object for its declared namespace to drive the layout.

Each emit-targets entry has a required type and can set target-specific options.

Option Typical use
output-dir Generated source output.
test-dir Generated target tests.
format Boolean toggle (default true). Runs the target formatter on emitted files when a formatter is available.
namespace-output "structural" or "flat". Defaults to structural and can override the global emitter setting per target.
import-path Import path used by generated tests or target modules.
package-name Go, Java, and Swift package or module naming.
namespace C# namespace, and the Java package when package-name is unset.
alias Target aliasing when supported by the emitter.
enum-parsing Rust enum parsing mode.
protocol-scaffolds Compile-only protocol scaffold generation.
outputs Internal output contributor requests identified by kind and optional provider. Existing target generation remains compatible with top-level options.
native-serialization Opt-in native interop surface. TypeScript supports "none" or "zod"; "standard-schema" is reserved and fails fast. Python supports "none" or "pydantic", Java supports "none" or "jackson", Rust supports "none" or "serde", and Swift supports "none" or "codable". Rust keeps its default cfg-gated serde surface when omitted; set "none" to opt out.
cancellation-token-path Full runtime-native cancellation token symbol path for generated runtimeCancellable method parameters. Rust uses :: paths; Python uses dotted module paths.

format defaults to true, but the emitter’s native (format: false) output is meant to be the byte-stable source of truth: running a language’s default formatter over the native tree should be a no-op, so generated bytes never depend on which formatter version a host happens to have installed.

A shared idempotency guard (scripts/idempotency-guard.mjs, wired into npm run validate:fixtures) copies each emitted native tree, runs the language’s default formatter over the copy, and asserts zero diff. When the formatter binary is absent the check skips loudly rather than passing silently. Per-runtime status is tracked declaratively in that module’s IDEMPOTENCY_TARGETS registry, which is the source of truth:

Status Runtimes Meaning
Locked (none yet) Formatter is a proven no-op; drift fails the guard.
Deferred Go, TypeScript (+zod), Python (+pydantic), Rust (+serde), Swift (+codable), Java (+jackson), C# Known formatter drift is recorded as a documented skipped-lock pending template alignment; the registry carries the measured gap and rationale.

An audit of native (format: false) output found that every runtime currently drifts under its default formatter, so no target is locked yet. Go is included: it ships format: true, and emitting its native output rewrites ~118/144 files under gofmt+goimports. The guard is therefore the enforcement scaffold plus the audit of record — promoting a runtime to Locked once its templates match its formatter is a one-line status flip that then fails on any regression.

Deferred targets can be measured live by setting TYPRA_IDEMPOTENCY_MEASURE=1 before validate:fixtures. Downstream format: false overrides remain supported; the guard only asserts that native output would survive its own formatter unchanged.

protocol-scaffolds: "compile-only" emits test-only implementations that prove generated protocols compile. They intentionally throw or reject when called and are not runtime fakes.

Native serialization is additional interop, not a replacement for Typra’s generated contract. Generated load/save methods remain authoritative for diagnostics, wire names, optional/default semantics, and raw payload preservation.

Native serialization mode support is validated per target. Unsupported combinations emit a TypeSpec diagnostic instead of silently falling back to plain generated output.

outputs is the projection seam for optional surfaces. It validates internal contributor requests and deduplicates compatibility options such as native-serialization. Python supports kind: server with provider: fastapi or provider: starlette, which emits route factories plus generated transport vector tests from TypeSpec HTTP metadata. Python also supports kind: consumer / provider: httpx, which emits contract-specific clients backed by an injected async transport. TypeScript supports kind: consumer / provider: fetch, which emits contract-specific fetch clients and generated consumer vector tests that can call any equivalent HTTP server projection. Unsupported target/kind/provider combinations fail with clear diagnostics instead of silently falling back.

Transport outputs share the same IR semantics: path/query/header/cookie/body bindings come from TypeSpec HTTP decorators, auth requirements are metadata-only, exact declared success statuses select success hydration, and non-matching or non-2xx responses stay on the error seam with their original status/body.