fable-compiler/ts2fable

Object type in union type gets split into its individual parts

Booksbaum opened this issue · 3 comments

export type Union1 = string | { name: string, age: number }

==>

type Union1 =
    U3<string, string, float>

-> name and age aren't a combined type, but split into individual cases

Probably should be something like

type Union1 = U2<string, Union1Case2>
type Union1Case2 =
  abstract name: string
  abstract age: float

An alternative could be to use an anonymous record:

type Union1 = U2<string, {| name: string; age: number |}>

That's way better!

Haven't thought about it: currently anonymous object types (in other locations) get converted into Interfaces -> just wanted to apply same conversion to union types.

I suggest we change all anon object types into anonymous records (like parameters or return values in functions).

But there's an issue: anon records are immutable -- but fields in anon object types are usually mutable:

{
  age: number  // mutable
  readonly birthday: number // immutable, but in anon objects rarely used
}

But most of the time anon object types are just used as data holder to pass around values, rather than to mutate data.
-> I think it's reasonable to use anon records and immutable properties.
( In fact: Before #391 (to add getter/setter for #348) properties in generated interfaces were immutable -> would not be a big change...and: interfaces with get,set are just cumbersome because they cannot be easily implemented with object expressions (cannot use val))



There's already a discussion about using anon records: #310 -> Decided against record types, because:

  • Immutable (vs. mutable JS) (but until recently: generated interfaces were already immutable)

  • Not great with optional values:

    • with Interfaces: use jsOptions to set fields, not used fields are omitted.
    • anon records (or object expression syntax with interfaces): must specify ALL fields, including optional ones. None fields still get generated (v = None -> v: void 0). But advantage: you have to specify required values -- with jsOptions you can ignore required fields.
      • object expression not great with mutable fields (But again: until recently generated interfaces were already immutable)
  • (by you)

    Another difference to take into account is that anonymous records should have structural equality by default even when boxed. Anonymous records created Fable will inherit the Record prototype but this won't be the case for objects coming from JS that are signed as records.

    I think that's not the case any more -- at least the generated code for anon records look like basic JS objects (unlike normal records): repl

Yes, in Fable 3 we changed equality so POJOs use structural equality. This was to support structural equality for anonymous records without having to inherit from Record. It has the side-effect that POJOs coming from JS will also trigger structural equality but maybe this is what F# users expect.

It's tricky, when POJOs are used in libraries to pass many options (as it's the case of many React libraries) it may be cumbersome to initialize a full anon record in F# (although I agree jsOptions is not ideal either). Maybe we can set an arbitrary number of fields to make the decision whether an anon object in .d.ts is translates as anon record or interface. Or just assume that in the case of objects with many fields, TS declarations will normally used a declared interface instead of an anon object type.