Decorators and wire names
Typra includes decorators for runtime concerns TypeSpec does not model by default.
| Decorator | Purpose |
|---|---|
@sample | Supplies generated test and example data. |
@abstract | Marks a model as not directly instantiated. |
@@coerce | Expands scalar input into an object during load. |
@@factory | Generates factory constructors for a model. |
@runtimeCancellable | Adds runtime-native cancellation to an operation without serializing it. |
@sync | Marks an operation as synchronously callable where targets support sync surfaces. |
@effect | Records operation effect metadata such as atomic and nonFatal. |
@optionalOperation | Marks an operation as optional on generated callable surfaces. |
@vector | Attaches callable behavior expectations to TypeSpec operations. |
@@knownAs | Maps a property to provider-specific wire names. |
@@defaultFor | Records provider-specific required defaults. |
@parseAlias | Accepts alternate input strings for a canonical union value. |
Wire names
Section titled “Wire names”@@knownAs(WireOptions.maxOutputTokens, "openai", "max_completion_tokens");@@knownAs(WireOptions.maxOutputTokens, "anthropic", "max_tokens");@@knownAs(WireOptions.temperature, "openai", "temperature");@@defaultFor(WireOptions.temperature, "openai", 0.2);
model WireOptions { maxOutputTokens?: int32; temperature?: float32;}Typra records provider-specific names so generated surfaces can load and save against external wire formats without changing the canonical TypeSpec property name.
Generated wire-name shape
Section titled “Generated wire-name shape”export class WireOptions { maxOutputTokens?: number | undefined; temperature?: number | undefined;
toWire(provider: string): Record<string, unknown> { const wireMap = { maxOutputTokens: { openai: "max_completion_tokens", anthropic: "max_tokens" }, temperature: { openai: "temperature" }, }; return Object.fromEntries( Object.entries(this.save()).flatMap(([key, value]) => wireMap[key]?.[provider] ? [[wireMap[key][provider], value]] : [] ) ); }}@dataclassclass WireOptions: max_output_tokens: int | None = None temperature: float | None = None
def to_wire(self, provider: str) -> dict[str, Any]: wire_map = { "maxOutputTokens": {"openai": "max_completion_tokens", "anthropic": "max_tokens"}, "temperature": {"openai": "temperature"}, } return { wire_map[key][provider]: value for key, value in self.save().items() if provider in wire_map.get(key, {}) }public Dictionary<string, object?> ToWire(string provider){ var wireMap = new Dictionary<string, Dictionary<string, string>> { ["maxOutputTokens"] = new() { ["openai"] = "max_completion_tokens", ["anthropic"] = "max_tokens" }, ["temperature"] = new() { ["openai"] = "temperature" }, }; return Save() .Where(item => wireMap.TryGetValue(item.Key, out var mapping) && mapping.ContainsKey(provider)) .ToDictionary(item => wireMap[item.Key][provider], item => item.Value);}func (obj *WireOptions) ToWire(provider string) map[string]interface{} { data := obj.Save(nil) wireMap := map[string]map[string]string{ "maxOutputTokens": {"openai": "max_completion_tokens", "anthropic": "max_tokens"}, "temperature": {"openai": "temperature"}, } result := make(map[string]interface{}) for key, value := range data { if wireName, ok := wireMap[key][provider]; ok { result[wireName] = value } } return result}public Map<String, Object> toWire(String provider) { Map<String, Object> result = new LinkedHashMap<>(); if ("openai".equals(provider) && this.maxOutputTokens != null) { result.put("max_completion_tokens", this.maxOutputTokens); } if ("anthropic".equals(provider) && this.maxOutputTokens != null) { result.put("max_tokens", this.maxOutputTokens); } if ("openai".equals(provider) && this.temperature != null) { result.put("temperature", this.temperature); } return result;}pub fn to_wire(&self, provider: &str) -> serde_json::Value { let wire_map = HashMap::from([ ("maxOutputTokens", HashMap::from([("openai", "max_completion_tokens"), ("anthropic", "max_tokens")])), ("temperature", HashMap::from([("openai", "temperature")])), ]); // Converts canonical fields to provider-specific names. serde_json::Value::Object(result)}public func toWire(_ provider: String, context: SaveContext = SaveContext()) throws -> [String: Any] { var result: [String: Any] = [:] let wireNameMaxOutputTokens: String switch provider { case "openai": wireNameMaxOutputTokens = "max_completion_tokens" case "anthropic": wireNameMaxOutputTokens = "max_tokens" default: wireNameMaxOutputTokens = "maxOutputTokens" } if let value = self.maxOutputTokens { result[wireNameMaxOutputTokens] = value } if let value = self.temperature { result["temperature"] = value } return result}{ "knownAs": [ { "property": "maxOutputTokens", "provider": "openai", "name": "max_completion_tokens" }, { "property": "maxOutputTokens", "provider": "anthropic", "name": "max_tokens" }, { "property": "temperature", "provider": "openai", "name": "temperature" } ], "defaults": [{ "property": "temperature", "provider": "openai", "value": 0.2 }]}Coercion and factories
Section titled “Coercion and factories”@@coerce( FixtureReference, string, #{ id: "{value}", label: "coerced reference" }, "reference", "Load a reference from an id string.", "ref-coerced");@@factory(FixtureReference, "named", #{ id: "{id}", label: "{label}" }, #{ id: "string", label: "string" });
model FixtureReference { id: string; label?: string;}@@coerce lets load paths expand a scalar input into an object. @@factory adds named construction helpers to generated target surfaces.
Generated coercion and factory shape
Section titled “Generated coercion and factory shape”export class FixtureReference { static readonly shorthandProperty: string | undefined = "id";
static load(data: Record<string, unknown>, context?: LoadContext): FixtureReference { if (typeof data === "string") { return new FixtureReference({ id: data, label: "coerced reference" }); } return new FixtureReference({ id: String(data["id"]), label: String(data["label"]) }); }
static named(id: string, label: string): FixtureReference { return new FixtureReference({ id, label }); }}@dataclassclass FixtureReference: _shorthand_property: ClassVar[str | None] = "id" id: str = "" label: str | None = None
@staticmethod def load(data: Any, context: LoadContext | None = None) -> "FixtureReference": if isinstance(data, str): return FixtureReference(id=data, label="coerced reference") return FixtureReference(id=data["id"], label=data.get("label"))
@staticmethod def named(id: str, label: str) -> "FixtureReference": return FixtureReference(id=id, label=label)public partial class FixtureReference{ public static string? ShorthandProperty => "id";
public static FixtureReference Load(object data, LoadContext? context = null) { if (data is string value) return new FixtureReference { Id = value, Label = "coerced reference" }; return Load(data.GetDictionary(ShorthandProperty), context); }
public static FixtureReference Named(string id, string label) => new() { Id = id, Label = label };}func LoadFixtureReference(data interface{}, ctx *LoadContext) (FixtureReference, error) { if s, ok := data.(string); ok { return FixtureReference{Id: s, Label: ptr("coerced reference")}, nil } // Load object shape when data is a map. return result, nil}
func NewFixtureReferenceNamed(id string, label string) FixtureReference { return FixtureReference{Id: id, Label: &label}}public class FixtureReference { public static final String SHORTHAND_PROPERTY = "id";
public static FixtureReference load(Object input, LoadContext context) { if (input instanceof String value) { FixtureReference result = new FixtureReference(); result.id = value; result.label = "coerced reference"; return result; } return loadObject(input, context); }
public static FixtureReference named(String id, String label) { /* ... */ }}impl FixtureReference { pub fn load_from_value(value: &serde_json::Value, ctx: &LoadContext) -> Self { if let Some(id) = value.as_str() { return Self { id: id.to_string(), label: Some("coerced reference".to_string()) }; } // Load object shape when value is an object. Self::default() }
pub fn named(id: String, label: String) -> Self { Self { id, label: Some(label) } }}public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> FixtureReference { if let scalar = data as? String { var instance = FixtureReference() instance.id = try TypraRuntime.string(scalar, field: "id") instance.label = "coerced reference" return instance } let object = try TypraRuntime.object(data, typeName: "FixtureReference") var instance = FixtureReference() if let value = object["id"] { instance.id = try TypraRuntime.string(value, field: "id") } if let value = object["label"] { instance.label = try TypraRuntime.string(value, field: "label") } return instance}
public static func named(id: String, label: String) -> FixtureReference { return FixtureReference(id: id, label: label)}{ "model": "FixtureReference", "coercions": [{ "from": "string", "template": { "id": "{value}", "label": "coerced reference" } }], "factories": [{ "name": "named", "parameters": { "id": "string", "label": "string" } }], "methods": [{ "name": "display", "returnType": "string" }]}Operation decorators and vectors
Section titled “Operation decorators and vectors”Use TypeSpec-native interfaces and operations for callable seams:
model Event { name: string;}
model EmitResult { accepted: boolean;}
interface EventSink { emit(event: Event): EmitResult;}Use @vector on operations when behavior should become cross-runtime conformance evidence:
const EmitVectors = #[ #{ name: "accept-event", stage: "event", input: #{ event: #{ name: "started" } }, expected: #{ accepted: true } }];
interface EventSink { @runtimeCancellable @vector(EmitVectors) emit(event: Event): EmitResult;
@sync format(event: Event): string;
@effect(#{ atomic: true }) append(event: Event): void;
@optionalOperation @effect(#{ nonFatal: true }) observe(event: Event): void;}JSON-string vector sets for keyword-named and opaque wire inputs
Section titled “JSON-string vector sets for keyword-named and opaque wire inputs”Vector input/expected values are opaque conformance evidence — Typra
serializes them into the vector snapshot and compares them structurally; they
are not type-checked against the operation’s parameters. TypeSpec object-value
literals (#{ ... }) require keys to be bare, non-keyword identifiers, so they
cannot express inputs whose domain models carry TypeSpec-keyword field names
(such as model) or that replay opaque provider wire payloads with arbitrary
keys. Quoting the key (#{ "model": ... }) is also rejected by the TypeSpec
grammar.
For those cases, author the vector set as a JSON string — typically a
triple-quoted string constant — which Typra parses into entries. The escape is
whole-set: mix native and JSON-string sets by stacking @vector decorators.
const WireVectors = """[ { "name": "wire-replay", "stage": "callable", "input": { "request": { "model": { "provider": "openai" } }, "response": { "id": "resp-1", "model": "gpt-4o-mini", "choices": [] } }, "expected": "Hello!" }]""";
interface Processor { @vector(WireVectors) process(request: CallRequest, response: unknown): unknown;}Operation decorators apply only to TypeSpec-native operations:
| Decorator | Runtime meaning |
|---|---|
@runtimeCancellable | Projects a host-owned cancellation parameter such as AbortSignal, CancellationToken, Go context.Context, or a configured runtime token path. It never becomes a model field, vector input, or serialized transport binding. |
@sync | Emits a synchronous signature in targets that distinguish sync and async callable surfaces. |
@effect(#{ atomic: true }) | Marks the operation as an atomic effect boundary for generated metadata, docs, and conformance evidence. Typra does not implement transactions. |
@effect(#{ nonFatal: true }) | Marks a post-commit or side-effect operation whose failures are non-fatal metadata. Host apps own error policy. |
@optionalOperation | Allows an optional/default implementation shape. Prefer smaller capability interfaces unless compatibility requires optional methods. |
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.