Skip to content

Unions and polymorphism

Typra supports closed string unions, open string unions, and discriminated model hierarchies.

@parseAlias("ready", #["complete"])
union FixtureStatus {
draft: "draft";
ready: "ready";
archived: "archived";
}

@parseAlias is parse-only. Loading accepts complete, but saving emits the canonical TypeSpec value ready.

String literal aliases keep their declared name when combined with null. For alias Mood = "calm" | "busy";, a field mood: Mood | null uses the generated type Mood, not mood. Regenerating code produced by 3.0.0 can change these property-derived type names; update any direct references to the old names.

Go and Swift declare an enum shared by several models once in their package or module. Rust also declares it once and imports it into the other model files. Use Rust’s generated module re-exports rather than depending on which model file owns the enum.

@parseAlias("batch", #["bulk"])
union FixtureMode {
interactive: "interactive";
batch: "batch";
custom: string;
}

Open unions preserve known values while allowing target-specific handling for custom strings.

@discriminator("kind")
model FixtureContent {
kind: string;
}
model TextContent extends FixtureContent {
kind: "text";
text: string;
}
model ImageContent extends FixtureContent {
kind: "image";
url: string;
}

Typra uses the discriminator field to load and save the right child shape. In the fixture contract, FixtureRoot.content and FixtureRoot.contentItems exercise both a single polymorphic value and a polymorphic collection.

Rust stores a single polymorphic reference as raw serde_json::Value. Saving its parent preserves that JSON without filling in nested defaults, including absent T | null = null fields. Direct model loading and typed polymorphic collections do apply those defaults. This limitation is unchanged.

export type FixtureMode = "interactive" | "batch" | (string & {});
export type FixtureStatus = "draft" | "ready" | "archived";
function parseFixtureStatus(value: unknown): FixtureStatus {
switch (String(value)) {
case "complete":
return "ready";
default:
throw new Error(`Invalid FixtureStatus value: ${value}`);
}
}
private static loadKind(data: Record<string, unknown>, context?: LoadContext): FixtureContent {
switch (String(data["kind"]).toLowerCase()) {
case "text":
return TextContent.load(data, context);
case "image":
return ImageContent.load(data, context);
default:
return new FixtureContent();
}
}