Skip to content

feat(ts): fields typed by an associated type - #644

Draft
kixelated wants to merge 2 commits into
mainfrom
quest/m3/ts-assoc-type-fields
Draft

kixelated wants to merge 2 commits into
mainfrom
quest/m3/ts-assoc-type-fields

Conversation

@kixelated

@kixelated kixelated commented Nov 7, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

rust2-ts refused a generic type whose field is typed by an associated type of its parameter, such as smallvec's Drain<'a, T: Array> holding slice::Iter<'a, T::Item>, with unsupported type <T as smallvec::Array>::Item. A type's definition is one TypeScript type over its type parameters, and a projection had no TypeScript name. Quest: quest/m3/ts-assoc-type-fields.md.

Approach

  • Each projection over a generic type's own parameters that its fields name becomes an extra TypeScript type parameter after the type's own. That covers a projection named directly, one held through another generic type of the crate (Batch<S> holding Drained<S> gets Batch<S, Item>), and a std iterator's item that only its Iterator impl names (Peekable<I>'s <I as Iterator>::Item). The parameter is named for the associated type and numbered past any name it would shadow (Ahead<_Item, _I, Item2, Item3>).
  • Each instance passes what the projection normalizes to there: Drained<[u8; 4]> is Drained<Uint8Array, rs.u8>. field_value's casts use the same arguments (Translator::instance_params).
  • An Option or Poll of a projection is an rs.OptionOf / rs.PollOf, the same as one of a parameter. generic_erasure now normalizes the instance's types and checks the projection as one unit, so it still refuses an instance whose erasure differs (Option<MaybeUninit<u32>>, Option<Option<()>>).
  • The list is computed by a pure walk of the fields (Translator::projections) before any field is typed. A projection the walk does not find stays unsupported type, so a gap is reported at compile time rather than producing bad output.

Evidence:

  • Differential case generics::projections in lang/shapes. It runs a struct, an enum that holds itself, a nested generic type, a std Peekable, and a projection from a projection, at arrays of u8, u16, (), and Option<u8>, and at a store that is no array, against native Rust.
  • Reject cases values::projected_uninit and values::projected_nested.
  • The model census (ts/moq/model.toml) at the moq pin drops from 52 refusals to 51. The removed one is smallvec's lib.rs:360 unsupported type <T as smallvec::Array>::Item. smallvec's remaining 10 are all unsafe (raw pointer behind a reference 6, union 2, Vec::from_raw_parts 2).

Impact

  • Translator output: a generic type whose fields name a projection gains one type parameter per projection, after its own. No type that translated before changes shape. Only the new fixture types and their callers appear in snapshots.
  • No runtime, config, or CLI change.

Choices made without asking

  • Naming: the associated type's own name (Item), then Item2, Item3… past any it would shadow. The alternative, S_Item-style names that always spell the parameter out, reads worse for the common single-projection case. Trait objects already name their associated types by bare name (Source<Item>).
  • Unions: they are included uniformly. An open-layout union never types its slots, so its projection parameter is unused and printed _Item, as an unused type parameter already is.
  • Dated census notes: the counts in quest/m2/ts-model/README.md and quest/m3/ts-unsafe/README.md are left as they are, since they are dated records of earlier censuses.

Alternatives

  • Per-trait conditional types (StoreItem<S> resolving by S): this would need one TypeScript type per trait, mapping every impl's self type. It cannot express impls on types that TypeScript erases alike ([u8; 4] and a Uint8Array view), and it breaks for blanket impls.
  • Discovering projections lazily while typing fields: a type that holds itself would be named with fewer arguments before a later field adds one. The eager walk avoids that.

Follow-ups

None.

Validation

  • just ci: lint passes, and 87 of 89 fixture tests pass. The 2 that fail are the TypeId-hash snapshots lang/dyns (ids.ts) and lang/panics/hook (lib.ts), whose hashes depend on the checkout's path. Both fail the same way on bare origin/main, CI passes them, and moq-dev/rust2#633 fixes them.
  • Model census before and after, as above.

Models

Size: M
Implementer: Claude Code, claude-opus-5-5, high effort

(Written by Claude Opus 5.5 with high effort)

🤖 Generated with Claude Code

@pullfrog

pullfrog Bot commented Nov 7, 2026

Copy link
Copy Markdown

Pullfrog  | Review this ➔

…meter

A projection a generic type's fields name, such as smallvec's
<A as Array>::Item, directly, through a generic type of the crate's they
hold, or as a std iterator's item, is a TypeScript type parameter of its own
after the type's, named for its associated type, which each instance gives
what the projection normalizes to there. A field's Option of one is an
rs.OptionOf, checked per instance as a parameter's is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title Fields typed by an associated type feat(ts): fields typed by an associated type Nov 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant