fix(types): enhance type system with mapped types and reverse-mapped …#2
Conversation
…types. Fix silent export drops - Introduced `MappedType` and `ReverseMappedType` to extend type system flexibility. - Updated `TypeFlagObject` and `STypeUnionBuilder` to account for commutative and ordered type assemblies. - Refined handling of index signatures and type mapping in dispatch logic. - Improved export resolution by grouping and sorting dependencies for better consistency.
Fixes
To do
Value-level modeling of
|
| Pattern | JS emission | Encoder effort |
|---|---|---|
as \${literal}${K}`` |
template string with $1 interpolation |
low — walk template parts |
as \${Capitalize<K & string>}`` etc. |
template + charAt/slice/toUpperCase
|
medium — table of intrinsi |
as K extends U ? never : K |
predicate function inline | medium — emit U-membership test |
as Conditional<K, ...> with non-never arms |
requires evaluating the conditional at runtime | high — lik |
| Arbitrary user-defined type-level fn | n/a | bail to opaque |
The first three tiers cover essentially all nameType use in real-world packages — the lib ones (Capitalize, Uncapitalize, Uppercase, Lowercase) and the canonical filtering idiom. The fourth tier is rare and the bail path catches it cleanly.
Implementation sketch:
| TypeNode.MappedType mappedTypeNode ->
match mappedTypeNode.nameType with
| Some nameType ->
match KeyRemap.classify ctx nameType with
| KeyRemap.TemplateLiteral parts ->
emitTemplateKeyConstructor ctx xanTag mappedTypeNode parts
| KeyRemap.NeverFilter predicate ->
emitFilteringKeyConstructor ctx xanTag mappedTypeNode predicate
| KeyRemap.TransformAndFilter (parts, predicate) ->
emitCombinedKeyConstructor ctx xanTag mappedTypeNode parts predicate
| KeyRemap.Unsupported ->
// Terminate body as Any; surrounding alias becomes `type Foo<'T> = obj`.
xanTag.TypeSignal
|> Signal.fulfillWith (fun () -> TypeSignal.ofKey TypeKindPrimitive.Any.TypeKey)
setTypeKeyFromNode mappedTypeNode
| None ->
routeViaChecker mappedTypeNodeKeyRemap.classify is the new code surface — a small AST classifier over nameType. Each branch emits a different shape of TypeLiteral with the appropriate static members.
Catches worth flagging
- The mapped type is no longer a plain TypeLiteral; it's a TypeLiteral plus static members. Implications for heritage —
interface X extends Getters<T>becomes weirder. But not worse than today: todayextends Getters<T>resolves to a lying IndexSignature F# can't honor anyway. The heritage rule (drop heritage for index-shaped targets, inline the me - The substrate intrinsics need extension for the
string-not-keyof<T>case. When the constraint is plainstring
Recommendation: two phases
-
Phase 1, lands with the encoder typar fix: node-level
nameTypedetection, terminate body asAny(do not forward to symbol declaration — for anonymous mapped types the symbol IS the enclosing alias, so a forward re-enters this branch). No key-constructor emission yet. Stops the silent lie and lets the existing substitution layer decide whether to map specific utility types toobj. Small change, immediate correctness win, no new substrate surface. -
Phase 2, separate PR: the
KeyRemap.classifymachinery and the three emission patterns. The value-level modeling. Earns its keep on packages that useGetters-style orOmit-via-neveridioms — real but not universal. Worth doing once Phase 1 stabilizes the error picture enough to measure Phase 2's actual coverage.
Splitting it this way keeps the encoder fix focused on "stop producing wrong output" first, then adds "produce useful output for cases we previously gave up on" as a follow-on. Each PR has a measurable error delta and a reviewable scope.
I think this is actually all baloney. There is absolutely no difficulty in having the encoder provide the required information. The information is fine to present as an index signature. The type slots for the index signature will be appropriately traversed and given. It will be up to the generator to either generate workarounds like those listed in the above plan, or to just default to obj. I would prefer that, over adding another type that needs to be pattern matched against. |
Reframed: encoder transmits, downstream decidesI overstated the case earlier. I was conflating two things:
(2) is true but it's downstream policy, not encoder concern. (1) is what I claimed but it's actually false — the IndexSignature is just a carrier. Its Parameter Type field can hold any The encoder already has the vocabularyThe encoded model can transmit remapped keys faithfully:
So the actual fix at | TypeNode.MappedType mappedTypeNode ->
let keyType =
match mappedTypeNode.nameType with
| Some nameTypeNode -> getTypeSignalFromNode nameTypeNode // f(K), faithfully
| None -> getTypeSignalFromNode mappedTypeNode.typeParameter.constraint // K
let valueType = getTypeSignalFromNode mappedTypeNode.``type``
let isReadonly = mappedTypeNode.readonlyToken |> Option.isSome
// Build IndexSignature with the real types. Encoder transmits, doesn't decide.
{ ... Parameters = [| { Name = "key"; Type = keyType; ... } |]
Type = valueType
IsReadOnly = isReadonly } |> SMemberBuilder.IndexSignature ...
setTypeKeyFromNode mappedTypeNodeThe encoded form now carries:
Same IndexSignature shape, full type information preserved. The encoder stops lying. Where the policy actually livesOnce the encoder transmits faithfully, the generator chooses how to render. Three options, all generator-side:
None of these require the encoder to know anything about F#'s representability. Reconciling with the earlier two-phase splitThe Phase 1 / Phase 2 split that put key-constructor emission in the encoder was wrong. The encoder doesn't emncoded model. The key-constructor pattern, if it's done at all, is a generator concern that reads the encoded Corrected split: Encoder fix (single, principled): Generator policy (optional, layered): the existing substitution layer (
The Record-collapse bug is solved by the encoder fix alone — the same patch covers Where the earlier framing went wrongI said the IndexSignature "can't carry f(K) keys, never-filtering, or co-varying V." That's wrong. It can carry any TsType in the Parameter Type slot. What it can't carry is the runtime invariant that the JS object's actual keys correspond to f(K) — but that's a runtime concern, and at the encoded-model layer there's no runtime, just the type graph. The encoder describes the type graph; whatever uses the encoded model decides what to do with the description. I conflated "structurally faithful encoded model" with "F#-renderable as a literal index signature." The first is the encoder's job and is achievable. The second is a separate question the generator answers, with the three options above — and the encoder isn't responsible for picking among them. |
Fix silent export drops
MappedTypeandReverseMappedTypeto extend type system flexibility.TypeFlagObjectandSTypeUnionBuilderto account for commutative and ordered type assemblies.