-
Notifications
You must be signed in to change notification settings - Fork 300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support tagged union types from TypeScript #2618
Support tagged union types from TypeScript #2618
Conversation
@cannorin Perhaps |
@ncave I think that's not a very good naming compared to |
For reference, this would enable #2543. |
|
For the record, I used [<TaggedUnion("kind")>]
type TypeScriptDU =
| [<Case("foo")>] Foo of {| kind: string; foo: string |}
| [<Case("bar")>] Bar of {| kind: string; bar: string |}
| [<Case("baz")>] Baz of {| kind: string; baz: string |} function f(x: DU) {
switch (x.kind) {
case 'foo':
console.log('foo: ' + x.foo); break;
case 'bar':
console.log('bar: ' + x.bar); break;
case 'baz':
console.log('baz: ' + x.baz); break;
}
} |
Hi @cannorin, thanks a lot for this! TBH I've been always a bit skeptical about trying to assimilate TS tagged unions with F# unions, moreover now that we're trying to make Fable more "language agnostic". But if it brings benefits to consume TS tagged unions in a more F# idiomatic way, we could do it if we can keep changes minimal and consistent with other union transformations (Erase, StringEnum). I'll try to reply to your comments below:
To keep the syntax clean I would use the union case field names to build the object instead of an anonymous record, and also omit the tagged field. I think at one point it was possible to use [<TaggedUnion("kind")>]
type TypeScriptDU =
| Foo of foo: string // { kind: "foo", foo }
| Bar of bar: string // { kind: "bar", bar }
| FooBar of foo: string * bar: string // { kind: "fooBar", foo, bar }
| [<CompiledName("__baz__")>] Baz of baz: string // { kind: "__baz__", baz }
Given that we're trying to extend Fable to other languages and this is a feature very specific to Typescript (not sure if it's applicable to other languages), maybe we could be entirely explicit about it with
Yes, as you noticed, the F# compiler already creates a "default" branch for the decision tree corresponding to one of the cases as it assumes the union is closed. I think the first case becomes the default one, but not sure if it's always like this. In any case, I think trying to match these open unions with F# unions is already a big stretch and forces too much the semantics to become useful. For these cases, I'd recommend to use interfaces and write/generate a helper for matching: type OpenUnion =
abstract kind: string
type OpenUnionFoo =
inherit OpenUnion
abstract foo: string
type OpenUnionBar =
inherit OpenUnion
abstract bar: int
[<AutoOpen>]
module OpenUnionExt =
type OpenUnion with
member this.Match() =
match this.kind with
| "foo" -> Choice1Of3(this :?> OpenUnionFoo)
| "bar" -> Choice2Of3(this :?> OpenUnionBar)
| _ -> Choice3Of3 this
let test (x: OpenUnion) =
match x.Match() with
| Choice1Of3 x -> x.foo
| Choice2Of3 x -> string x.bar
| Choice3Of3 _ -> "default"
If you use an Erased union Fable will already use type Foo() =
member _.Foo = "foo"
type Bar() =
member _.Bar = "bar"
[<Erase>]
type T = Foo of Foo | Bar of Bar
let test = function
| Foo x -> x.Foo
| Bar x -> x.Bar |
@alfonsogarciacaro good point!
|
1527047
to
a69e20d
Compare
Ah, you're right. That makes sense! 👍
The docs say patter matching with erased unions will be compiled as type testing, although it's true it doesn't show an specific example with classes. In any case, I prefer not to "promote" too much erased unions in the docs because I noticed some users think they are more performant than "standard" unions and try to use them all the time which causes issues because type testing has many pitfalls in Fable. |
@alfonsogarciacaro I just realized that the value of tag field can also be a number, an enum case, or a boolean, where type Kind = Foo = 0 | Bar = 1 | Baz = 2
type Base =
abstract kind: Kind
type Foo =
inherit Base
abstract foo: string
type Bar =
inherit Base
abstract bar: int
type Baz =
inherit Base
abstract baz: bool
[<RequireQualifiedAccess; TypeScriptTaggedUnion("kind")>]
type EnumTagged =
| [<CompiledValue(Kind.Foo)>] Foo of Foo
| [<CompiledValue(Kind.Bar)>] Bar of Bar
| [<CompiledValue(Kind.Baz)>] Baz of Baz
|
Awesome work @cannorin, thanks a lot! |
@alfonsogarciacaro I haven't moved the quick tests to the appropriate test file yet 😅 Can you revert the merge so that I can do that? |
@alfonsogarciacaro I saw your commit moving the tests, thank you! |
Testing this, looks great! A couple of remarks:
|
@alfonsogarciacaro Thanks!
|
Created a follow-up PR: #2623 |
Summary
This PR adds support for the "tagged" union types from TypeScript.
Details:
TypeScriptTaggedUnion
respectsCaseRule
if given.CompiledName
attribute.CompiledValue
attribute is used instead ofCompiledName
.int
,float
,bool
, and enum types of 32bit integer values.TODOs: