Building Types & Enums
This chapter covers type declarations (classes, structs, interfaces, enums, type aliases, newtypes), computed properties, annotations, and enum variants. These specs follow the same builder pattern described in Building Functions & Fields: mut self for setters that return Self, self for .build(), and fluent chaining: Builder::new(...).method().method().build().
TypeSpec
The largest spec. Models type declarations: struct, class, interface, trait, enum, type alias, or newtype wrapper. Takes a TypeKind to select the declaration form.
.build() returns Err(SigilStitchError::DuplicateFieldName { type_name, field_name }) when two fields in the same type share a name.
Single-block output (TypeScript class)
The TypeScript adapter lowers a class and its members into one CodeBlock:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
fn main() {
let body = CodeBlock::of("return this.name", ()).unwrap();
let type_spec = TypeSpec::builder("UserService", TypeKind::Class)
.visibility(Visibility::Public)
.add_field(
FieldSpec::builder("name", TypeName::primitive("string"))
.visibility(Visibility::Private)
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("getName")
.returns(TypeName::primitive("string"))
.body(body)
.build()
.unwrap(),
)
.build()
.unwrap();
let blocks = type_spec.emit(&TypeScript::new()).unwrap();
// blocks.len() == 1
//
// export class UserService {
// private name: string;
//
// getName(): string {
// return this.name
// }
// }
}
Two-block output (Rust struct + impl)
The Rust adapter lowers a struct with methods into two CodeBlocks: one for the
data definition and one for the impl block:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::rust::Rust;
fn main() {
let body = CodeBlock::of("Self { name: name.to_string() }", ()).unwrap();
let type_spec = TypeSpec::builder("Config", TypeKind::Struct)
.visibility(Visibility::Public)
.add_field(
FieldSpec::builder("name", TypeName::primitive("String"))
.visibility(Visibility::Public)
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("new")
.visibility(Visibility::Public)
.add_param(ParameterSpec::new("name", TypeName::primitive("&str")).unwrap())
.returns(TypeName::primitive("Self"))
.body(body)
.build()
.unwrap(),
)
.build()
.unwrap();
let blocks = type_spec.emit(&Rust::new()).unwrap();
// blocks.len() == 2
//
// Block 0:
// pub struct Config {
// pub name: String,
// }
//
// Block 1:
// impl Config {
// pub fn new(name: &str) -> Self {
// Self { name: name.to_string() }
// }
// }
}
The split is target grammar owned by the adapter. The TypeSpec records the
same declaration intent without describing whether members are nested in the
type or emitted in a separate implementation block.
Extends and implements
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("AdminService", TypeKind::Class)
.visibility(Visibility::Public)
.extends(TypeName::primitive("BaseService"))
.implements(TypeName::primitive("Serializable"))
.build()
.unwrap();
// export class AdminService extends BaseService implements Serializable {
// }
}
Embedded types (Go struct composition)
Use add_embedded(TypeName) for unnamed type references inside a struct body. This models Go’s embedded field pattern where a type is included by name without a field identifier:
extern crate sigil_stitch;
use sigil_stitch::lang::go::Go;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("UserAdmin", TypeKind::Struct)
.add_embedded(TypeName::primitive("User"))
.add_embedded(TypeName::primitive("Admin"))
.add_field(
FieldSpec::builder("Role", TypeName::primitive("string"))
.build()
.unwrap(),
)
.build()
.unwrap();
// type UserAdmin struct {
// User
// Admin
// Role string
// }
}
Embedded types render before regular fields. If the embedded type is TypeName::importable(...), its import is tracked automatically via %T. This works across languages — for Go interfaces, embedded types produce interface composition:
extern crate sigil_stitch;
use sigil_stitch::lang::go::Go;
use sigil_stitch::prelude::*;
fn main() {
let io_reader = TypeName::importable("io", "Reader");
let io_writer = TypeName::importable("io", "Writer");
let type_spec = TypeSpec::builder("ReadWriter", TypeKind::Interface)
.add_embedded(io_reader)
.add_embedded(io_writer)
.build()
.unwrap();
// type ReadWriter interface {
// io.Reader
// io.Writer
// }
}
Type aliases
TypeKind::TypeAlias emits a single-line type alias declaration with no body. The aliased target is set via .extends() (exactly one required). No fields, methods, or variants are allowed.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
use sigil_stitch::lang::rust::Rust;
fn main() {
// TypeScript: export type UserId = string;
let type_spec = TypeSpec::builder("UserId", TypeKind::TypeAlias)
.visibility(Visibility::Public)
.extends(TypeName::primitive("string"))
.build()
.unwrap();
// Rust: pub type Meters = f64;
let type_spec = TypeSpec::builder("Meters", TypeKind::TypeAlias)
.visibility(Visibility::Public)
.extends(TypeName::primitive("f64"))
.build()
.unwrap();
}
Each language adapter owns the complete type-alias form:
- TypeScript/Rust:
type Foo = Bar; - C++:
using Foo = Bar; - C:
typedef Bar Foo; - Go:
type Foo = Bar - Kotlin:
typealias Foo = Bar - Python:
type Foo = Bar
Type aliases support type parameters:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
// Rust: pub type Result<T> = std::result::Result<T, MyError>;
let type_spec = TypeSpec::builder("Result", TypeKind::TypeAlias)
.visibility(Visibility::Public)
.add_type_param(TypeParamSpec::new("T"))
.extends(TypeName::generic(
TypeName::primitive("std::result::Result"),
vec![TypeName::primitive("T"), TypeName::primitive("MyError")],
))
.build()
.unwrap();
}
Newtype wrappers
TypeKind::Newtype emits a single-line newtype wrapper. Like type aliases, the inner type is set via .extends() (exactly one required).
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::rust::Rust;
use sigil_stitch::lang::go::Go;
fn main() {
// Rust: pub struct Meters(f64);
let type_spec = TypeSpec::builder("Meters", TypeKind::Newtype)
.visibility(Visibility::Public)
.extends(TypeName::primitive("f64"))
.build()
.unwrap();
// Go: type Meters float64
let type_spec = TypeSpec::builder("Meters", TypeKind::Newtype)
.extends(TypeName::primitive("float64"))
.build()
.unwrap();
}
Newtype syntax varies across languages and is owned by each adapter’s
declaration lowering. Lowering preserves the inner TypeName as a structured
reference, so imports and aliases work inside newtype declarations just as they
do in ordinary %T slots:
- Rust:
struct Meters(f64);(tuple struct) - Go:
type Meters float64(distinct type) - Kotlin:
value class Meters(val value: f64)(inline class) - Python:
Meters = NewType("Meters", float)(typing.NewType) - C:
typedef float Meters;(typedef)
Rust, Go, Haskell, Kotlin, and Scala adapters emit supported type parameters and bounds. C, PHP, and Python omit them because their native wrapper forms do not support declaration-site generic parameters.
Enums with EnumVariantSpec
TypeSpec with TypeKind::Enum uses add_variant() instead of add_field(). See the EnumVariantSpec section below for variant forms.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
use sigil_stitch::lang::typescript::TypeScript;
fn main() {
let type_spec = TypeSpec::builder("Direction", TypeKind::Enum)
.add_variant(
EnumVariantSpec::builder("Up")
.discriminant(CodeBlock::of("'UP'", ()).unwrap())
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("Down")
.discriminant(CodeBlock::of("'DOWN'", ()).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
// enum Direction {
// Up = 'UP',
// Down = 'DOWN',
// }
}
PropertySpec
PropertySpec describes a computed value with read and/or write behavior. It
records the value type, accessor bodies, visibility, static intent,
documentation, and annotations without choosing a target syntax. At emission,
the selected adapter validates PropertyIntent against its context-specific
profile and completely lowers the accepted declaration:
- TypeScript and JavaScript emit native accessor declarations.
- Swift emits a
varcomputed property, including getter-only properties. - Kotlin emits a
valorvarfollowed directly by its indented accessors; there is no outer property brace. - PHP emits
getName()andsetName()methods. - Scala emits
def nameanddef name_=methods.
Other built-ins reject the unsupported property context instead of falling
back to plausible target text. Swift and Kotlin require an explicit value type
and read accessor. Kotlin and Scala reject static property intent.
TypeScript interfaces and Swift protocols also reject PropertySpec: those
targets support bodyless property requirements, while this spec carries
concrete accessor bodies and never discards them.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::spec::property_spec::PropertySpec;
use sigil_stitch::lang::typescript::TypeScript;
fn main() {
let getter_body = CodeBlock::of("return this._name", ()).unwrap();
let setter_body = CodeBlock::of("this._name = value", ()).unwrap();
let prop = PropertySpec::builder("name", TypeName::primitive("string"))
.getter(getter_body)
.setter("value", setter_body)
.build()
.unwrap();
// TypeScript:
// get name(): string {
// return this._name
// }
// set name(value: string) {
// this._name = value
// }
}
PropertySpec::emit() retains its pre-0.6.8 direct facade and accepts a
DeclarationContext. Adding the property to TypeSpec supplies the owning
TypeKind, which lets the adapter reject invalid contract or declaration
contexts before lowering. External adapters written against 0.6.8 retain the
deprecated PropertyStyle compatibility behavior; new adapters implement
validate_property() and lower_property() instead. The complete deprecated
surface and migration paths are listed in 0.6.8 Legacy Compatibility and
Migration.
When a target lowers properties into a namespace shared with other members,
TypeSpec also supplies one validation-only TypeMembersIntent after all
per-family checks. Exact duplicate property names are rejected by the crate;
the adapter rejects names that collide only after its own lowering. PHP uses
this pass because method names are case-insensitive and generated getName()
or setName() accessors can collide with accessors from another property or
with an explicit method. TypeScript, Kotlin, Swift, and Scala use the same seam
for their own field/property namespaces; only declarations in the same
target-local namespace collide, so TypeScript private names and TypeScript and
Swift static members stay distinct from their instance counterparts.
TypeScript, Swift, and Scala also reject explicit methods that occupy the same
emitted member name in that namespace. These are separate adapter rules, not
one general namespace abstraction. This owner-wide view does not change the
per-property PropertyIntent -> ValidatedProperty -> lower_property() path.
AnnotationSpec
Structured annotations that render with language-appropriate syntax. The prefix and suffix adapt automatically:
| Language | Syntax |
|---|---|
| Java, Kotlin, TS | @Name(args) |
| Rust | #[name(args)] |
| C++ | [[name(args)]] |
| C | __attribute__((name(args))) |
extern crate sigil_stitch;
use sigil_stitch::spec::annotation_spec::AnnotationSpec;
use sigil_stitch::lang::rust::Rust;
use sigil_stitch::prelude::*;
fn main() {
// Simple annotation: #[allow(dead_code)]
let ann = AnnotationSpec::new("allow").arg("dead_code");
// Multiple arguments: #[cfg(test, feature = "nightly")]
let ann = AnnotationSpec::new("cfg")
.arg("test")
.arg("feature = \"nightly\"");
// Bulk arguments from an iterator: #[derive(Debug, Clone, Serialize)]
let ann = AnnotationSpec::new("derive")
.args(["Debug", "Clone", "Serialize"]);
}
For import-tracked annotations, use importable() with a TypeName:
extern crate sigil_stitch;
use sigil_stitch::spec::annotation_spec::AnnotationSpec;
use sigil_stitch::lang::typescript::TypeScript;
use sigil_stitch::type_name::TypeName;
use sigil_stitch::prelude::*;
fn main() {
let type_name = TypeName::importable("./decorators", "Component");
let ann = AnnotationSpec::importable(type_name);
// TS: @Component (with import { Component } from './decorators')
}
If AnnotationSpec does not cover your annotation format, every builder also has an .annotation(CodeBlock) escape hatch that accepts a raw CodeBlock.
EnumVariantSpec
Variants are validated and lowered as one owner-aware sequence through
TypeSpec. This lets the selected language derive first/last position, choose
valid separators, and terminate the variant section when fields or methods
follow. Direct positional emission with VariantContext is deprecated and is
rejected by strict built-in adapters. See 0.6.8 Legacy Compatibility and
Migration for direct-facade and builder
replacements.
Individual enum variants. Five forms are supported:
Simple variant
extern crate sigil_stitch;
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
use sigil_stitch::lang::rust::Rust;
use sigil_stitch::prelude::*;
fn main() {
let v = EnumVariantSpec::new("Red").unwrap();
// Rust: Red,
}
Discriminated variant
extern crate sigil_stitch;
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
use sigil_stitch::lang::typescript::TypeScript;
use sigil_stitch::prelude::*;
fn main() {
let variant = EnumVariantSpec::builder("Up")
.discriminant(CodeBlock::of("'UP'", ()).unwrap())
.build()
.unwrap();
// TypeScript: Up = 'UP',
}
Use .constructor_argument(...) instead when an enum entry invokes its
declaring enum’s constructor, as in Java or Kotlin. Discriminants, constructor
arguments, positional payload types, and record payload fields are distinct
semantic forms and cannot be combined on one variant. The deprecated
.value(...) builder remains only for 0.6.8 compatibility and is rejected when
the selected language cannot give it one validity-preserving meaning.
Enum-entry constructor arguments (Java, Kotlin)
extern crate sigil_stitch;
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
use sigil_stitch::prelude::*;
fn main() {
let variant = EnumVariantSpec::builder("ACTIVE")
.constructor_argument(CodeBlock::of("\"active\"", ()).unwrap())
.build()
.unwrap();
// Java/Kotlin: ACTIVE("active")
}
The owning enum must also declare a compatible structured constructor (or Kotlin primary constructor). sigil-stitch checks every enum entry against the accepted argument-count ranges of structured constructors, including overloads, defaulted parameters, and variadic parameters. Opaque extra members remain an escape hatch whose target-language constructor signatures cannot be inferred.
Structured variant annotations are accepted only when the adapter can preserve
declaration-metadata semantics. Ruby therefore rejects AnnotationSpec on enum
constants instead of rendering it as a comment; .annotation(CodeBlock) remains
an explicit escape hatch for target-specific Ruby code.
Positional payload (Rust, Swift)
extern crate sigil_stitch;
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
use sigil_stitch::lang::rust::Rust;
use sigil_stitch::prelude::*;
fn main() {
let variant = EnumVariantSpec::builder("Literal")
.positional_payload(TypeName::primitive("i64"))
.build()
.unwrap();
// Rust: Literal(i64),
// Multi-element tuple
let variant = EnumVariantSpec::builder("Pair")
.positional_payload(TypeName::primitive("String"))
.positional_payload(TypeName::primitive("i32"))
.build()
.unwrap();
// Rust: Pair(String, i32),
}
Record payload (Rust)
extern crate sigil_stitch;
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
use sigil_stitch::spec::field_spec::FieldSpec;
use sigil_stitch::lang::rust::Rust;
use sigil_stitch::prelude::*;
fn main() {
let variant = EnumVariantSpec::builder("Move")
.record_payload_field(
FieldSpec::builder("x", TypeName::primitive("i32")).build().unwrap(),
)
.record_payload_field(
FieldSpec::builder("y", TypeName::primitive("i32")).build().unwrap(),
)
.build()
.unwrap();
// Rust:
// Move {
// x: i32,
// y: i32,
// },
}
Variants are added to a TypeSpec via add_variant(). The language adapter
owns their complete grammar, including separators, trailing punctuation, and
prefixes such as Swift’s case. The pre-0.6.8 builder names
.associated_type(...) and .add_field(...) remain as deprecated aliases for
.positional_payload(...) and .record_payload_field(...), respectively.