Fable: CaseRules.LowerFirst of keyValueList is applied after [<CompiledName>] attribute

Created on 7 Sep 2018  路  10Comments  路  Source: fable-compiler/Fable

Description

When using keyValueList the rule CaseRules.LowerFirst is applied after [<CompiledName>] attribute.
I am not sure if this is a bug. But if not, how do I provide a specific compile name?

Repro code

open Fable.Core
open Fable.Core.JsInterop

type MyType =
    | [<CompiledName("PascalCase")>] PascalCase of string

let myList = [ PascalCase "value" ]

let myObj = keyValueList CaseRules.LowerFirst myList
printfn "%O" myObj

Expected and actual results

_Expected:_ {"PascalCase":"value"}
_Actual:_ {"pascalCase":"value"}

Related information

  • Fable version (dotnet fable --version): 1.3.8 (in REPL2 the behavior is the same)
  • Operating system: Windows 10

Most helpful comment

The problem is that keyValueList isn't working only at compile time but at runtime time. And we currently have no information that the property was generated using CompiledName. Adding this info would increase the bundle size and need an extract check per properties convertion and could reduce performance.

If you are designing an API which is prefixed. It's easy to hide the Custom under the hood for the end user.

module Component =

    type Props =
        | [<Erase>] Custom of string * obj
        | Width of float

    let SpecialPropA (h : float) = Custom ("SpecialPropA", h)

    // By adding a generic arg you can inline the call in the user coe
    let inline SpecialPropB<'a> (h : float) = Custom ("SpecialPropB", h)

let x =
    [ Component.Width 3.
      Component.SpecialPropA 5.
      Component.SpecialPropB 5. ]

And like that the API is consistent and easy to explore for the user

All 10 comments

You can use CaseRules.None instead. We can open this to discussion, it may be possible to change the behaviour at compile time but it will be difficult at runtime because currently it's not possible to know from JS if the name comes from the case name or the CompiledName attribute.

@mvsmal Until you find the workaround, you can always just do the conversion manually:

[<Emit("$2[$0] = $1")>]
let setProp (key: string) (value: obj) (objectLiteral: obj) : unit = jsNative

type Options =
| PascalCase of string
| CamelCase of string

let createLiteral (opts: Options list) = 
    let emptyLiteral = obj() 
    let combine state elem = 
        match elem with 
        | PascalCase value -> 
            setProp "PascalCase" value state 
            state 
        | CamelCase value -> 
            setProp "camelCase" value state
            state 
    List.fold combine emptyLiteral opts

let custom = createLiteral [ PascalCase "first"; CamelCase "second" ]

Fable.Import.Browser.console.log(custom) 

// { PascalCase: "first", camelCase: "second" }

Thanks @Zaid-Ajaj, but it is not an option, I have too many cases. I actually found a workaround I think.

type Options =
| [<Erase>] Custom of string * obj

let myList = [ Custom ("PascalCase", "value") ]

Erased cases keep their letter casing.

However I would encourage you to rethink the approach, it is quite confusing that [<CompiledName>] is not accurate 馃槙

Well, it's accurate in a sense :) The compiled name in JS is whatever you have in the attribute (which btw I'm not sure it happens in .NET F#) but then you call keyValueList with the meaning "build a JS object using the following list of union cases representing key and value pairs applying this case rules to the key names". The way to compile case names and the keyValueList helper are two different things, albeit related.

I use keyValueList for Reacts props. I have DUs implementing IHTMLProp with hundreds of cases in total, also HTMLAttr could be passed in the same list since it implements IHTMLProp as well.

The problem is that Material-UI, which I have bindings for, sometimes needs props exactly in PascalCase. So in this case I don't have an option except HTMLAttr.Custom but then I cannot provide a DU case for such prop as e.g. ModalProps and users must use Custom too.

With [<CompiledName>] that would absolutely straightforward.
Hope you get my point.

The problem is that keyValueList isn't working only at compile time but at runtime time. And we currently have no information that the property was generated using CompiledName. Adding this info would increase the bundle size and need an extract check per properties convertion and could reduce performance.

If you are designing an API which is prefixed. It's easy to hide the Custom under the hood for the end user.

module Component =

    type Props =
        | [<Erase>] Custom of string * obj
        | Width of float

    let SpecialPropA (h : float) = Custom ("SpecialPropA", h)

    // By adding a generic arg you can inline the call in the user coe
    let inline SpecialPropB<'a> (h : float) = Custom ("SpecialPropB", h)

let x =
    [ Component.Width 3.
      Component.SpecialPropA 5.
      Component.SpecialPropB 5. ]

And like that the API is consistent and easy to explore for the user

Thank you @MangelMaxime, I didn't think about this solution. So let's close this issue then.

Thanks a lot for the suggestion @MangelMaxime! I just edited your code to add the [<Erase>] attribute to Custom.

BTW, keyValueList is resolved at compile time when possible but this is not always the case so as you say we do need to take always runtime resolution in mind.

@mvsmal Sorry, I forgot. There was actually a similar problem with React Native and the solution was to use a different union type for the rules that must be in Pascal Case, then in the helper to build the options object, you can split the lists and use different CaseRules for each.

@alfonsogarciacaro Ah yes good catch :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

krauthaufen picture krauthaufen  路  3Comments

alfonsogarciacaro picture alfonsogarciacaro  路  3Comments

rommsen picture rommsen  路  3Comments

ncave picture ncave  路  3Comments

theprash picture theprash  路  3Comments