#1140·neon

TypeScript: nicer synthesized names for composed boxed types (Rust-type-based)

Author: dhermanCreated Jun 15, 2026Updated Jun 15, 2026

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 (from string | undefined | null)
  • Arc<Vec<u8>>Boxednumber (from number[])

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>>BoxedOptionString
  • Arc<Vec<u8>>BoxedVecU8
  • Arc<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

  1. 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 is std::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).

  2. type_name is 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 in doc/typescript.md), and (b) only composed boxed-type names are affected — named-type names are stable regardless.

  3. 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[]BoxedNumber hides the array-ness).
  • Semantic Or/And encoding (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.