Generate artifact metadata
TypeScript erases parameter types when it compiles a standard decorator. Generate server metadata from your artifact sources to keep the method declaration as the source of truth. The Tasks sample builds both client proxies and server metadata from the same project:
yarn tsc -b Source/Core Source/Tools/ProxyGeneratoryarn workspace @cratis/arc.core.sample.tasks generate-proxiesyarn buildThe script passes --project, --artifacts, --output, and --metadata Samples/Tasks/Features/generatedMetadata.ts to arc-proxygenerator. The generated module is TypeScript, checked into the sample, and compiled along with the artifacts. yarn build checks its content against the current source and fails with a regenerate artifact metadata message when it is missing, changed, or stale. yarn ci checks the committed module before regenerating, so stale metadata fails CI. Run the generator before a tsx development server or Vitest run that uses the artifacts; do not rely on the test runner to discover erased types.
Import the generated module and install it before discovery or add(), as in the Tasks bootstrap. The builder.useGeneratedMetadata(metadata) call must precede builder.discover(...).
Now the sample command uses handle(tasks: Tasks) without @inject(Tasks), and a read-model method can use @query() static taskById(id: TaskId, tasks: Tasks). Arc analyzes argument names and positions, concrete service classes (including nullable services, which receive null when unregistered), nullable/optional fields, validator targets, and the declared observable return. A union of two concrete service classes needs an explicit token. Keep @field(Type) on every wire property: that is the Cratis runtime schema convention, not redundant generated metadata. provide() still supplies one unmarked value as the first handle() argument; typed provided(Type) remains available for explicitly bound additional values.
For a development loop, run arc-proxygenerator with the same options plus --watch. It debounces changes under the artifacts root and watches referenced local source files outside that root. Run it in a separate terminal before your tsx or Vitest command; do not run a watched generator as a one-shot CI step. Use --check-metadata with the same arguments for a read-only build gate. The programmatic generateFromSource({ project, artifacts, output, metadata }) writes the same module, while renderGeneratedMetadata(project, artifacts, metadataFile) returns its deterministic text without writing it. Paths in the programmatic generator are absolute, and the output folder must exist.
Explicit decorators remain useful when you cannot run a build step: @inject(Tasks) and @query(argument('id', TaskId), service(Tasks)) override inferred bindings. They also let you inject an interface by giving Arc a concrete class or serviceToken to resolve. The analyzer cannot invent a runtime value for an interface or erased type; it reports the file and line and asks for an explicit token. @validator(Target) similarly overrides inferred validator targets. Legacy experimentalDecorators with emitDecoratorMetadata still handles decorated class-valued parameters without generation; standard decorators need either generated metadata or explicit bindings. Bare @query() in standard mode does not give a TS1241 parameter-count error without generation; build() rejects unbound parameters instead.
For commands, generated handleResult describes the client response after server-handled values are removed. When handle() returns events, operations, or a tuple with a handled value, handleValueResult separately retains the declared raw return cardinality for runtime validation; a returned event is not a client response. Regenerate both proxies and metadata together when changing command return types.
The generated module records a version and each class’s runtime-checkable shape (fields, method names, and arities). useGeneratedMetadata rejects an incompatible version or shape at build time. Reordering parameters without changing their count can silently bind the wrong dependency. Always run --check-metadata after editing an artifact, and retain class names when bundling (keepNames in esbuild). Runtime reflection cannot see source-only changes such as parameter types or positions. Client-only generation without --metadata must opt into source-inferred bindings with --use-generated-metadata (or generatedMetadata: true programmatically); the presence of a generated file does not switch modes implicitly. Do not edit the generated module; regenerate it. The analyzer supports concrete exported class tokens, primitives, concepts, arrays of wire values, and declared query observable sources. This is a bounded source-analysis path, not full parity with the .NET Roslyn generators; see capabilities.