glutinum-org / cli

https://glutinum.net/
58 stars 4 forks source link

How to represents union types with literals values and general accepted type? #27

Open MangelMaxime opened 9 months ago

MangelMaxime commented 9 months ago

TypeScript documentation:

export type ForegroundColor =
    | 'black'
    | 'red'
    | string

The first intuition to represents that type is to use StringEnum however this is not possible because of the general accepted string case.

One solution could be to generated a specialised Erased type:

[<RequireQualifiedAccess>]
module ForegroundColor =

    [<StringEnum(CaseRules.None)>]
    [<RequireQualifiedAccess>]
    type Literal =
        | black
        | red

[<RequireQualifiedAccess>]
[<Erase>]
type ForegroundColor =
    | Literal of ForegroundColor.Literal
    | String of string

Usage:

let red = ForegroundColor.Literal ForegroundColor.Literal.red

let custom = ForegroundColor.String "black"

Another representation could be:

open Fable.Core

[<RequireQualifiedAccess>]
module ForegroundColor =

    [<StringEnum(CaseRules.None)>]
    type Literal =
        | [<CompiledName("black")>] Black
        | [<CompiledName("red")>] Red

[<RequireQualifiedAccess>]
[<Erase>]
type ForegroundColor =
    | Literal of ForegroundColor.Literal
    | String of string

let red = ForegroundColor.Literal ForegroundColor.Red

let custom = ForegroundColor.String "black"

Note: We needed to use CompiledName because DU cases in F# can only use lower case if marked with [<RequireQualifiedAccess>].

The problem with this second representation is that it differs from Glutinum philosophy to trying to state close the native API values for literal values.

Example of npm package using this features:

Moment

goswinr commented 9 months ago

@MangelMaxime The type you linked in Moment.Js is only used once as parameter. If such a type is only used as a parameter, not as a return value, an overload might be more F# idiomatic: So the generated type is not implementing the Literal and the String case, but the function using this type as argument implements overloads:

[<StringEnum(CaseRules.None)>]
type ForegroundColor =
    | [<CompiledName("black")>] Black
    | [<CompiledName("red")>] Red

type MyText = // overloads for `Literal` and the `String` case:
    abstract Draw: text: string * ?color: ForegroundColor -> unit
    abstract Draw: text: string * ?color: string -> unit

It would be used like this: (On an instance of let t = MyText() )

t.Draw("Hi Glutinum!", "green")
t.Draw("Hi Glutinum!", Black)

instead of:

t.Draw("Hi Glutinum!", ForegroundColor.String "green")
t.Draw("Hi Glutinum!", ForegroundColor.Literal ForegroundColor.Black)

REPL

MangelMaxime commented 9 months ago

You are right, the point you are is a similar case as the one described in https://github.com/glutinum-org/cli/issues/29 (you could not know about it because I only got the time to create it now).

I am still unsure if we should or should not keep the original Erased type definition for API reference.

goswinr commented 9 months ago

Discussion continues here: https://github.com/glutinum-org/cli/issues/29#issuecomment-1884383465