Skip to content

Interfaces, operations, and transport

Typra uses TypeSpec-native interface and op declarations for callable seams. Models remain the data contract; operations describe how host-owned runtime code is called; operation decorators describe runtime-only details that should not become serialized model fields.

import "@typra/emitter";
model RenderRequest {
prompt: string;
}
model RenderResult {
text: string;
}
interface Renderer {
render(request: RenderRequest): RenderResult;
}
TypeSpec Typra mapping
interface Renderer A callable contract named Renderer.
render(request: RenderRequest) A generated callable operation with a typed request parameter.
RenderResult return type A typed success result loaded through generated model helpers.

Generated targets project this shared callable contract into idiomatic protocol, interface, trait, or stub surfaces. Host apps provide runtime behavior behind the generated seam.

interface Renderer {
@runtimeCancellable
render(request: RenderRequest): RenderResult;
@sync
preview(request: RenderRequest): string;
@effect(#{ atomic: true })
commit(result: RenderResult): void;
@optionalOperation
@effect(#{ nonFatal: true })
observe(result: RenderResult): void;
}
Decorator Typra mapping
@runtimeCancellable Adds a host-owned cancellation parameter, such as AbortSignal, CancellationToken, Go context.Context, or a configured runtime token path.
@sync Marks the operation as synchronously callable where the target has distinct sync/async surfaces.
@effect(#{ atomic: true }) Records that the operation is an atomic effect boundary. Typra records metadata; the host app owns transaction policy.
@effect(#{ nonFatal: true }) Records that operation failures are non-fatal metadata. The host app owns failure handling policy.
@optionalOperation Marks the operation as optional on generated callable surfaces.

These decorators are operation metadata. They do not alter model load/save shape, do not become transport parameters, and do not make Typra responsible for transactions, retries, token acquisition, credential storage, or observability.

Use @vector when an operation needs executable conformance evidence. Vectors can be authored inline or loaded from a constant vector set.

const RenderVectors = #[
#{
name: "basic-render",
stage: "render",
input: #{ request: #{ prompt: "Hello, Typra" } },
expected: #{ text: "Hello, Typra" }
}
];
interface Renderer {
@vector(RenderVectors)
render(request: RenderRequest): RenderResult;
}

Typra lowers vectors into language-neutral conformance records. Generated tests obtain a runtime-supplied adapter for each operation, invoke it with the vector input, and compare each target’s observable transcript against the vector. Every @vector-bound operation must be implemented by a thin adapter or carry a tracked waiver — a missing adapter hard-fails the generated suite rather than skipping silently. See Vector conformance adapters for the registration contract. Vector input/expected are opaque evidence, compared structurally rather than typed against the operation’s parameters or round-tripped through model load()/save(), so authored inputs may be sparse (omitting required-with-default fields the operation ignores). Model-typed data fidelity is proven separately by @sample data conformance.

The @protocol and @method model-level decorators were removed in Typra 2.0.0. Some existing Typra contracts may still describe callable seams with those model-level decorators. Convert those declarations to TypeSpec-native interfaces and operations before adding new coverage.

Previous decorator form TypeSpec-native form
@@protocol(Renderer); model Renderer {} interface Renderer { ... }
@@method(Renderer, "render", "RenderResult", "...", #{ request: "RenderRequest" }) render(request: RenderRequest): RenderResult;
optional: true argument on @@method @optionalOperation on the operation.
sync: true argument on @@method @sync on the operation.
#{ runtimeCancellable: true } options @runtimeCancellable on the operation.
#{ atomic: true } options @effect(#{ atomic: true }) on the operation.
#{ nonFatal: true } options @effect(#{ nonFatal: true }) on the operation.

Before:

model RenderRequest {
prompt: string;
}
model RenderResult {
text: string;
}
@@protocol(Renderer);
@@method(
Renderer,
"render",
"RenderResult",
"Render a prompt.",
#{ request: "RenderRequest" },
false,
false,
#{ runtimeCancellable: true }
);
@@method(
Renderer,
"observe",
"void",
"Observe a rendered result.",
#{ result: "RenderResult" },
true,
false,
#{ nonFatal: true }
);
model Renderer {}

After:

model RenderRequest {
prompt: string;
}
model RenderResult {
text: string;
}
interface Renderer {
@runtimeCancellable
render(request: RenderRequest): RenderResult;
@optionalOperation
@effect(#{ nonFatal: true })
observe(result: RenderResult): void;
}

Conversion rules:

  1. Keep the existing model names and parameter names unless they conflict with TypeSpec operation syntax.
  2. Replace string type names in @@method parameters and returns with real TypeSpec type references.
  3. Move runtime-only booleans and effect options onto operation decorators.
  4. Keep @vector data on the operation that owns the behavior case.
  5. Keep HTTP metadata on TypeSpec HTTP decorators such as @route, @path, @query, @header, @cookie, @body, and @statusCode.

Transport projection consumes official TypeSpec HTTP metadata. Typra does not add a separate route or parameter vocabulary.

import "@typespec/http";
using TypeSpec.Http;
@route("/renders")
interface RenderApi {
@post
create(
@header requestId: string,
@cookie session: string,
@body request: RenderRequest
): {
@statusCode statusCode: 201;
@body result: RenderResult;
} | {
@statusCode statusCode: 400;
@body error: RenderError;
};
}
model RenderError {
message: string;
}
HTTP construct Typra mapping
@route, @get, @post, etc. Producer and consumer route/verb metadata.
@path URI path binding.
@query Query string binding.
@header Header binding.
@cookie Cookie binding.
@body Request or response body binding.
@statusCode Exact response status selection.

Generated consumers hydrate only the declared success response body for the matching status. Non-matching 2xx responses and non-2xx responses remain on the error seam with the original status and body, so error payloads are not loaded as success models.

export interface Renderer {
render(request: RenderRequest, signal?: AbortSignal): Promise<RenderResult>;
preview(request: RenderRequest): string;
commit(result: RenderResult): Promise<void>;
observe?(result: RenderResult): Promise<void>;
}