Fable icon indicating copy to clipboard operation
Fable copied to clipboard

[Enhancement] StringEnum can opt-in to respect CompiledValueAttribute's on cases

Open shayanhabibi opened this issue 6 months ago • 6 comments

  • Adds optional boolean switch as second positional argument which toggles StringEnum respecting CompiledValue attributes.
  • Adds tests for StringEnum respecting CompiledValue
  • Adds remarks to xml docs to add this usage possibility

Example

Before:

[<StringEnum(CaseRules.LowerFirst)>]
type Behavior =
    | Replace
    | Combine
    | [<CompiledValue(false)>] None

console.log Behavior.None // 'none'
console.log Behavior.Replace // 'replace'

Now:

[<StringEnum(CaseRules.LowerFirst, true)>]
type Behavior =
    | Replace
    | Combine
    | [<CompiledValue(false)>] None

console.log Behavior.None // false
console.log Behavior.Replace // 'replace'

shayanhabibi avatar Jun 22 '25 09:06 shayanhabibi

How do I manage the error with the standalone test build failure?

shayanhabibi avatar Jun 22 '25 10:06 shayanhabibi

I not 100% sure but I think the Fable Standalone error is because we need to re-generate the metadata dll.

But before that what is the need behind this change?

I am not a fan of having a switch in StringEnum which then do something if we add another attribute somewhere else. It doesn't feels easy to understand to the user.

Potentially, we could decide that CompiledValue take precedence over StringEnum and so user would just need to add CompiledValue without having to pass a flag in StringEnum

MangelMaxime avatar Jun 22 '25 12:06 MangelMaxime

Only because it could be 'potentially' a change which impacts code bases. I, for instance, was not aware that compiled value didnt work with StringEnums, and only found when debugging some errors. But more importantly, I don't know how this would effect things like TypeScript generation. Judging from your comment though, this is something you would be fine with? Honestly my preference is to just have CompiledValue take precedence too. Perhaps since we're doing a major version change, this is a better time than any to make the change. What do you think?

As for the reason behind this change, it is simply the fact that many enums in javascript land may consider true or false as independent special values, while at all other times they are strings.

shayanhabibi avatar Jun 22 '25 13:06 shayanhabibi

But more importantly, I don't know how this would effect things like TypeScript generation.

I don't use TypeScript generation personally so I am not sure, but from my perspective if the CI is green then it "probably" means we didn't break anything.

Honestly my preference is to just have CompiledValue take precedence too.

Can you please do it this way?

We also need to add tests for all the different values that CompiledValue supports and check that the output is supported by JavaScript/TypeScript.

As for the reason behind this change, it is simply the fact that many enums in javascript land may consider true or false as independent special values, while at all other times they are strings.

Not 100% related but this feels similar to https://github.com/glutinum-org/cli/issues/111

And, from what I know I don't think we have a way in pure Fable/F# (without emit) to represent these types.

MangelMaxime avatar Jun 22 '25 15:06 MangelMaxime

Tried to check [<CompiledValue(null)>] against JS.undefined but got this:

image

so I removed that test.

Everything else works as expected.

shayanhabibi avatar Jun 22 '25 16:06 shayanhabibi

Regarding the failing CI, I will consider it normal and we are going to disable the test for Fable Standalone.

This is because on JavaScript we can't make the distinction between an int or a float:

match box 42 with
| :? int as value -> printfn "Int"
| :? float as value -> printfn "Int"
| _ -> printfn "Unknown"

compiles to

import { printf, toConsole } from "fable-library-js/String.js";

(function () {
    const matchValue = 42;
    if (typeof matchValue === "number") {
        const value = matchValue | 0;
        toConsole(printf("Int"));
    }
    else if (typeof matchValue === "number") {
        const value_1 = matchValue;
        toConsole(printf("Int"));
    }
    else {
        toConsole(printf("Unknown"));
    }
})();

There are ways to have a better distinction, but it will never be able to make a difference between 3 and 3.0 for example

MangelMaxime avatar Jun 24 '25 19:06 MangelMaxime