Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TS] Emit interface declaration when encountering a ParamObject pattern #4007

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

MangelMaxime
Copy link
Member

@MangelMaxime MangelMaxime commented Jan 8, 2025

Fix #4003

[<AllowNullLiteral>]
[<Global>]
type User
    [<ParamObjectAttribute; Emit("$0")>]
    (id : int, ?name : string, ?age: int) =
    member val name: string option = jsNative with get, set

let term =
    User(1, "test")

transpile to

export interface User {
    id: int32,
    name?: string,
    age?: int32
}

export const term: User = {
    id: 1,
    name: "test",
};

Still need to add integration tests, supports generics, inheritance.

@MangelMaxime
Copy link
Member Author

@ncave

When generating type for

[<AllowNullLiteral>]
[<Global>]
type TestWithGenerics<'T, 'A>
    [<ParamObjectAttribute; Emit("$0")>]
    ( _name : 'T, ?age: int) =
    member val name: string option = jsNative with get, set

let test2 : TestWithGenerics<string, obj> =
    TestWithGenerics("", 0)

I get this output:

export interface TestWithGenerics$2<T, A> {
    _name: T,
    age?: int32
}

export const test2: TestWithGenerics<string, any> = {
    _name: "",
    age: 0,
};

Do you have an idea on why we don't get the same name for TestWithGenerics class ?

@ncave
Copy link
Collaborator

ncave commented Jan 8, 2025

@MangelMaxime Possibly a difference between using a type's CompiledName vs DisplayName

@MangelMaxime
Copy link
Member Author

I will try to investigate more.

@Freymaurer @joprice

Basic cases seems to be easy enough to support, but there is more complexes cases caused by the fact that ParamObject use the name of the argument for generating the POJO property but nothing in F# force the user to use the same name between inheritance or multiple constructors.

Inheritance

[<AllowNullLiteral>]
[<Global>]
type BaseClass
    [<ParamObjectAttribute; Emit("$0")>]
    ( name : string) =
    member val name: string option = jsNative with get, set

[<AllowNullLiteral>]
[<Global>]
type DerivedClass
    [<ParamObjectAttribute; Emit("$0")>]
    ( name : string, age : int) =
    inherit BaseClass(name)
    member val name: string option = jsNative with get, set

Which should generate something like:

export interface BaseClass {
    name: string
}

export interface DerivedClass extends BaseClass {
    age: int32
}

export const derived: DerivedClass = {
    name: "name",
    age: 42,
};

However, this F# code is also valid:

[<AllowNullLiteral>]
[<Global>]
type BaseClass
    [<ParamObjectAttribute; Emit("$0")>]
    ( name : string) =
    member val name: string option = jsNative with get, set

[<AllowNullLiteral>]
[<Global>]
type DerivedClass
    [<ParamObjectAttribute; Emit("$0")>]
    ( thisIsAnotherName : string, age : int) =
    inherit BaseClass(thisIsAnotherName)
    member val name: string option = jsNative with get, set

and now the ParamObject with generate a property of name thisIsAnotherName instead of name. I guess, we could add some Fable validation to force the user to use the same argument name?

Depending on how difficult adding Fable validation is, we can also mark it to do later.

Multiple constructors

Another situation is how to support multiple constructors:

[<AllowNullLiteral>]
[<Global>]
type Multiple
    [<ParamObjectAttribute; Emit("$0")>]
    private ( numberOrString : U2<string, int>) =

    [<ParamObjectAttribute; Emit("$0")>]
    new (numberOrString : string, arg: int) =
        Multiple(U2.Case1 numberOrString)

    [<ParamObjectAttribute; Emit("$0")>]
    new (numberOrString : int) =
        Multiple(U2.Case2 numberOrString)

let multiple1 =
    Multiple("test", 1)

let multiple2 =
    Multiple(1)

We could generate something like that:

export type Multiple = {
  numberOrString: string | int32
} | {
  numberOrString: string,
  arg: int32,
} | {
  numberOrString: int32
}

export const multiple1: Multiple = {
  numberOrString: "test",
  arg: 1,
};

export const multiple2: Multiple = {
  numberOrString: 1,
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[TypeScript] POJO with [<ParamObject>] generates missing type hint.
2 participants