Repository navigation
feat(js/ts): add InlineInterface for TypeScriptTaggedUnion cases - #5113
Open
shayanhabibi wants to merge 2 commits into
Open
shayanhabibi wants to merge 2 commits into
shayanhabibi wants to merge 2 commits into
Conversation
Since fable-compiler#3844, a TypeScriptTaggedUnion case whose single field is an interface compiles to `{ tag, Item }`, so TypeScript code reads `command.Item.fromIndex` rather than the documented `Take & { type: "take" }`. [<InlineInterface>] on a case, or on the union type for every case with fields, compiles the case as its interface with the tag added: - the case type is `Take & { type: "take" }`; - reading the field returns the union value; - construction copies the value with its prototype and sets the tag on the copy, so the caller's object is unchanged and class members keep working; the value is evaluated once; - matching and `IsTake` are unchanged. A case with InlineInterface must have exactly one field, whose type is an interface; otherwise construction and field reads report an error. Fable.AST: `UnionCase` gains `Attributes`, so Fable2Babel can read a case-level attribute when building the type annotation. Refs fable-compiler#5078: this is the compiler half; the docs page still needs updating. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LySb2Kjdc8qoY9Qoikr2u5
The standalone compiler compiles against the committed metadata assembly, which did not yet contain InlineInterfaceAttribute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LySb2Kjdc8qoY9Qoikr2u5
shayanhabibi
added a commit
to shayanhabibi/Fable
that referenced
this pull request
Oct 20, 2026
Adds InlineInterface (fable-compiler#5113) next to CaseTest. The fable-metadata Fable.Core.dll is rebuilt from the merged Fable.Core so it carries both attributes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LySb2Kjdc8qoY9Qoikr2u5
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the compiler part of #5078.
What
Since #3844 (4.19.1), a
TypeScriptTaggedUnioncase whose single field is an interface compiles to{ type, Item }, so TypeScript code readscommand.Item.fromIndex. The docs still describe the earlier shape,Take & { type: "take" }. This PR adds an opt-in attribute,Fable.Core.InlineInterfaceAttribute, that produces the documented shape. Default output is unchanged.How
InlineInterfaceAttribute(), valid on a case or on the union type. This is purely an addition.UnionCasegainsAttributes: Attribute seq, asEntity,MemberFunctionOrValueandParameteralready have.Fable2Babelbuilds the type annotation fromFable.UnionCase, so without this it could only see the attribute on the type, not on a case. Adding an abstract member breaks any code outside Fable that implementsFable.UnionCase. Code that only reads it is unaffected, and I'm not aware of any implementations outsideFsUnionCase.Transforms.Util.tryInlinedInterface: one check, shared byFSharp2FableandFable2Babel, that decides whether a case is inlined. A case qualifies when the attribute is on it or on its type and the case has exactly one field, whose type is an interface. On the type, cases without fields are skipped.Field & { tag: "..." }.Take tbindstto the value itself (aTypeCast).Object.assign(Object.create(Object.getPrototypeOf(t)), t, { type: "take" }). The value is evaluated once, bound to a temporary when it could have side effects.IsTake: unchanged; they already test the tag.[<InlineInterface>] Count of int, or a fieldless case carrying the attribute itself) reports an error at construction and field reads. A primitive can't carry a tag, which was Missing discriminator field when using TypeScriptTaggedUnion #3837.FsUnionCase.HasNamedFieldscomment is removed.Construction: why a prototype-preserving copy
#5078 listed three options and asked for input. None came, so here is the reasoning:
tas is) is the pre-Fixes #3837 #3844 behaviour, and leaves values built in F# without a tag (Missing discriminator field when using TypeScriptTaggedUnion #3837).Object.assign(t, { type })) writes to an object the caller owns. Constructing a union case shouldn't change its argument.{ ...t, type }) leaves the caller's object alone. But it copies own properties only, so an F# class instance implementing the interface loses its members, which live on the prototype.The copy here takes the prototype along. For a plain object it gives the same result as a spread. For a class instance it keeps the members working. The caller's object stays unchanged, and only object identity differs. I'm happy to switch to a plain spread if that's preferred.
Notes
InlineInterfaceis a working name, as in the issue.Item) output and point to this attribute. I'll open that on fable-compiler.github.io once the name is settled.src/fable-metadata/lib/Fable.Core.dllis updated so the standalone compiler sees the attribute, as feat(js/ts): add CaseTest attribute for TypeScriptTaggedUnion cases that share a tag #5076 did.Tests
tests/Js/Main/JsInteropTests.fshas four new tests:Item, the caller's object is unchanged), matching, a raw JS object,IsTake, and a non-inlined case in the same union;tests/Integration/Compiler/CompilerMessagesTests.fshas two new tests: a case with anintfield reports an error, and a valid union compiles with no warnings. All 131 compiler tests pass../build.sh test javascriptandtest typescript: everything passes except the twoDateTimeOffset.ToString('d'/'D')tests, which also fail onmainin my time zone (UTC+8)../build.sh test integration: 41/42. The failure ispathMap, which expects/and gets\on Windows; it's unrelated to this PR.tsc --strict, both from source and through a DLL.JsInteropTests.fsisn't part of the TypeScript test project, so I checked this separately.🤖 Generated with Claude Code
https://claude.ai/code/session_01LySb2Kjdc8qoY9Qoikr2u5