glutinum-org / cli

https://glutinum.net/
59 stars 6 forks source link

Unsupported kind ConstructorType error when type alias contains new #130

Closed joprice closed 3 weeks ago

joprice commented 1 month ago

Issue created from Glutinum Tool

Glutinum version - 0.11.0-preview

TypeScript

import * as pg from "pg";

  declare class Pool<T extends pg.Client> extends pg.Pool {
      readonly Client: Pool.ClientLikeCtr<T>;

      constructor(config?: Pool.Config<T>, client?: Pool.ClientLikeCtr<T>);

      connect(): Promise<T & pg.PoolClient>;
      connect(callback: (err?: Error, client?: T & pg.PoolClient, done?: (release?: any) => void) => void): void;

      on(event: "error", listener: (err: Error, client: T & pg.PoolClient) => void): this;
      on(event: "connect" | "acquire" | "remove", listener: (client: T & pg.PoolClient) => void): this;
  }

  declare namespace Pool {
      type ClientLikeCtr<T extends pg.Client> = new(config?: string | pg.ClientConfig) => T;

      interface Config<T extends pg.Client> extends pg.PoolConfig {
          Client?: ClientLikeCtr<T> | undefined;
      }
  }

  export = Pool;

FSharp (with warnings/errors)

module rec Glutinum

open Fable.Core
open Fable.Core.JsInterop
open System

[<AbstractClass>]
[<Erase>]
type Exports =
    [<Import("Pool", "REPLACE_ME_WITH_MODULE_NAME"); EmitConstructor>]
    static member Pool<'T when 'T :> pg.Client> (?config: Pool.Config<'T>, ?client: Pool.ClientLikeCtr<'T>) : Pool<'T when 'T :> pg.Client> = nativeOnly

[<AllowNullLiteral>]
[<Interface>]
type Pool<'T when 'T :> pg.Client> =
    inherit pg.Pool
    abstract member Client: Pool.ClientLikeCtr<'T> with get
    abstract member connect: unit -> JS.Promise<obj>
    abstract member connect: callback: (Error -> obj -> (obj -> unit) -> unit) -> unit
    abstract member on: event: string * listener: (Error -> obj -> unit) -> Pool
    abstract member on: event: Pool.on.event * listener: (obj -> unit) -> Pool

module Pool =

    type ClientLikeCtr<'T when 'T :> pg.Client> =
        obj

    [<AllowNullLiteral>]
    [<Interface>]
    type Config<'T when 'T :> pg.Client> =
        inherit pg.PoolConfig
        abstract member Client: ClientLikeCtr option with get, set

    module on =

        [<RequireQualifiedAccess>]
        [<StringEnum(CaseRules.None)>]
        type event =
            | connect
            | acquire
            | remove

[!WARNING]

./src/Glutinum.Converter/Reader/TypeNode.fs(397): Error while reading type node from:
/index.d.ts(16,48)

Unsupported kind ConstructorType

--- Text ---
 new(config?: string | pg.ClientConfig) => T
---

--- Parent text ---

      type ClientLikeCtr<T extends pg.Client> = new(config?: string | pg.ClientConfig) => T;
---
/index.d.ts(16,48)

Unsupported kind ConstructorType

--- Text ---
 new(config?: string | pg.ClientConfig) => T
---

--- Parent text ---

      type ClientLikeCtr<T extends pg.Client> = new(config?: string | pg.ClientConfig) => T;
---

Problem description

When new appears in a type alias, it fails to get handled and results in the fsharp type obj. The above is taken from pg-pool. A simpler version that triggers this is:

type Z = new() => number
MangelMaxime commented 1 month ago

I don't think we have an equivalent in F#, do we?

joprice commented 1 month ago

I don't think so. It's basically a function that is capturing a compile time proof that it can be called with new. Scalajs uses an implicit ConstructorTag trait along with a a helper function, where the trait can be used as a type bound similar to what can be done with SRTP:

Perhaps jsConstructor or a similar intrinsic could be designed to enable a comparable typesafe pattern, but as it currently stands, the example above new(config?: string | pg.ClientConfig) => T; would just have to be a function that doesn't track that it's source is a constructor.

MangelMaxime commented 3 weeks ago
type ClientLikeCtr<T extends pg.Client> = new(config?: string) => T;

now generates

type ClientLikeCtr<'T when 'T :> pg.Client> =
    obj

but this is not a valid F# code.

We could instead generate:

type ClientLikeCtr = obj

But we lose the generic type information and also when encountering a TypeReference Glutinum doesn't have access to the full type information. This is because, a type use itself then an infinite loop occurs. Because of that, we would not be able to apply the optimisation of removing the generics from the type when the type is used as a type signature.

To workaround that, what we can do is for

interface Client {
    port : number
}

type ClientLikeCtr<T extends Client> = new(config?: string) => T;

declare class Pool<T extends Client> {
    Client: ClientLikeCtr<T>;
}

generates

[<AllowNullLiteral>]
[<Interface>]
type Client =
    abstract member port: float with get, set

[<Erase>]
type ClientLikeCtr<'T when 'T :> Client> =
    ClientLikeCtr of 'T

[<AllowNullLiteral>]
[<Interface>]
type Pool<'T when 'T :> Client> =
    abstract member Client: ClientLikeCtr<'T> with get, set

When using [<Erase>] on a single case DUs Fable, will use the underlying type directly. Doing so allow to make F# compiler happy and still have access to the generics information.

[<Erase>]
type Test<'T> = Test of 'T

let value = Test "dwd"

let (Test.Test v) = value

printfn "%A" v

generates

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

export const t = "dwd";

export const patternInput$004010 = t;

export const v = patternInput$004010;

toConsole(printf("%A"))(v);

The draw back is that the user will need to unwrap the DU case to access the value. To improve the situation we could generates a

[<Erase>]
type Test<'T> = 
    | Test of 'T

    member inline this.Value =
        let (Test.Test v) = this
        v      

let value = Test "dwd"

printfn "%A" value.Value // unwrapping is hidden to the user