Currently ts-gen maps TypeScript template literal types to String in Rust, which is correct but loses the structural information encoded in the type. For example, from the Cloudflare Workflows API:
type WorkflowDurationLabel = "second" | "minute" | "hour" | "day" | "week" | "month" | "year";
type WorkflowSleepDuration = `${number} ${WorkflowDurationLabel}${"s" | ""}` | number;
This is currently emitted as String, but the template literal has enough structure to generate a typed builder.
Setter naming rules
- Named type references get a setter named after the type in snake_case, taking the generated enum by value:
.workflow_duration_label(WorkflowDurationLabel::Second)
- Primitive types (
number, string) and anonymous unions (${"s" | ""}) get positional setters: .set_0(...), .set_1(...), .set_2(...)
- The first positional slot additionally gets a
.set(...) convenience alias for .set_0(...)
- Anonymous string union slots default to their first value, so
${"s" | ""} defaults to "s" without the user needing to set it. Primitive types do not default.
Constructor and builder
A new() constructor is generated taking all non-defaultable slots positionally, covering the common case concisely. The builder exists for when defaults need to be overridden.
// common case
WorkflowSleepDuration::new(5, WorkflowDurationLabel::Minute)
// => "5 minutes"
// override a default
WorkflowSleepDuration::builder()
.set(5)
.workflow_duration_label(WorkflowDurationLabel::Minute)
.set_2("") // override the default "s"
.build()
The literal separator between slots ( in this case) is a fixed string baked into build() and new(). Both return String.
Type evaluation before codegen
Template literals often contain TypeScript utility types in their slots, e.g. Capitalize<T>. Since ts-gen already resolves type references, these should be eagerly evaluated against their concrete instantiations before builder generation — by the time Rust is emitted all slots are concrete string literals or primitives. The complexity stays in the IR resolution phase, not the codegen phase.
General pattern
The structure ${A}${literal}${B}${literal}${C} maps cleanly to a new() constructor and builder with one slot per interpolation regardless of the number of slots or separator literals. This covers a meaningful subset of real-world template literal types in the CF Workers API surface (durations, URLs, event name patterns, etc.), with String remaining the fallback for cases that can't be reduced to this pattern.
Relationship to #1
The generated builder would follow the same builder pattern described in #1 for object types, and could potentially share the same build() optimisation path once that lands.
Currently ts-gen maps TypeScript template literal types to
Stringin Rust, which is correct but loses the structural information encoded in the type. For example, from the Cloudflare Workflows API:This is currently emitted as
String, but the template literal has enough structure to generate a typed builder.Setter naming rules
.workflow_duration_label(WorkflowDurationLabel::Second)number,string) and anonymous unions (${"s" | ""}) get positional setters:.set_0(...),.set_1(...),.set_2(...).set(...)convenience alias for.set_0(...)${"s" | ""}defaults to"s"without the user needing to set it. Primitive types do not default.Constructor and builder
A
new()constructor is generated taking all non-defaultable slots positionally, covering the common case concisely. The builder exists for when defaults need to be overridden.The literal separator between slots (
in this case) is a fixed string baked intobuild()andnew(). Both returnString.Type evaluation before codegen
Template literals often contain TypeScript utility types in their slots, e.g.
Capitalize<T>. Since ts-gen already resolves type references, these should be eagerly evaluated against their concrete instantiations before builder generation — by the time Rust is emitted all slots are concrete string literals or primitives. The complexity stays in the IR resolution phase, not the codegen phase.General pattern
The structure
${A}${literal}${B}${literal}${C}maps cleanly to anew()constructor and builder with one slot per interpolation regardless of the number of slots or separator literals. This covers a meaningful subset of real-world template literal types in the CF Workers API surface (durations, URLs, event name patterns, etc.), withStringremaining the fallback for cases that can't be reduced to this pattern.Relationship to #1
The generated builder would follow the same builder pattern described in #1 for object types, and could potentially share the same
build()optimisation path once that lands.