TypeScript: nicer synthesized names for composed boxed types (Rust-type-based)
Background
Follow-up from #1137. When an opaque boxed type (Arc<T> / Rc<T> / RefCell<T> / Boxed<T>) wraps a composed type rather than a named one, the synthesized branded-interface name is derived by sanitizing the inner TypeScript type expression. For example:
Arc<Database>→BoxedDatabase✅ (the common, intended case — already good)Arc<Option<String>>→Boxedstringundefinednull(fromstring | undefined | null)Arc<Vec<u8>>→Boxednumber(fromnumber[])
The sanitizer (added in #1137) guarantees the output is always a valid identifier, so no invalid .d.ts is ever emitted. But the names for composed types are ugly and don't convey structure. This issue tracks making them nicer.
Proposal: derive the name from the Rust type
Base the synthesized name on the Rust type being boxed rather than the TypeScript type:
Arc<Option<String>>→BoxedOptionStringArc<Vec<u8>>→BoxedVecU8Arc<Database>→BoxedDatabase(unchanged)
This is more recognizable (it maps back to what the user wrote), and it preserves Rust's nominal distinctions, which matches JsBox downcast semantics better than the TS-based name (e.g. Arc<Vec<u8>> and Arc<Vec<i32>> are genuinely distinct boxes that can't be downcast to each other; TS-based naming collapses both to Boxednumber).
Considerations / tradeoffs
Source of the name. Boxing is a library blanket impl (
impl<T> TypeScript for Arc<T>), not macro codegen, so by the time the name is synthesized the only runtime source of the Rust type name isstd::any::type_name::<T>(). We'd parse/normalize its output (core::option::Option<alloc::string::String>→OptionString): strip module paths, walk generics, concatenate constructor names, with sanitization fallback for exotic shapes (refs, slices, tuples,dyn, lifetimes).type_nameis explicitly unstable. Its format "is not specified" and "may change between versions of the compiler." Since these names land in generated.d.ts, a rustc upgrade could churn them. This is the main reason it was deferred rather than landed in #1137. Mitigated by: (a) the output format is documented as not-yet-semver-stable (see the Stability section indoc/typescript.md), and (b) only composed boxed-type names are affected — named-type names are stable regardless.Name/brand consistency. Today the interface name and the brand value (
[__neon_tag]: '...') both derive from the same string (the TS type), giving the invariant "same name ⟹ same brand ⟹ identical declaration" (safe dedup). If the name moves to a Rust-derived source while the brand stays TS-derived, that invariant breaks and two different inner types could sanitize to the same name with different brands → conflicting declarations. To keep it clean, both the name and the brand should move to the Rust-derived source together. That's a broader change to the boxed-type identity model, which is why this deserves its own focused change.
Scope
- Only affects composed/anonymous boxed types; named-type output is unchanged.
- Not a Rust-API break (any new trait hook would have a default impl); it's a generated-output change, which the documented Stability policy already permits.
Alternatives considered (in #1137 discussion)
- Keep current sanitized lowercase mash (
Boxedstringundefinednull) — simplest; the ugliness is arguably a mild "don't box composed types" signal, and the brand carries the real identity. - Uniform PascalCase pass (
BoxedStringUndefinedNull) — one rule, no semantic interpretation, but still lossy (number[]→BoxedNumberhides the array-ness). - Semantic
Or/Andencoding (BoxedStringOrUndefinedOrNull) — prettiest for unions, but selectively faithful (no clean prose form for arrays/records/tuples/objects → scope creep) and over-promises structural semantics on a nominal/opaque type.
Rust-based naming was judged the best of the "make it meaningful" options, deferred only for the type_name stability + brand-model reasons above.
Source: neon-bindings/neon