Skip to content

Commit

Permalink
[TS] Generate interface type when using the "ParamObject" class pat…
Browse files Browse the repository at this point in the history
…tern

* [TS] Emit `interface` declaration when encountering a `ParamObject` pattern

* Try fix condition

* Forward generics information

* Keep comments for ts interface generation ✨ (#4020)

* Keep comments for ts interface generation ✨

* Update QuickTest.fs, fix typo

* Update changelog and rmv print statements

* Generate the correct type name when assigning a `ParamObject` to a variable

* Add support for inheritance and multi shaped ParamObject

* Update changelog entry

* Revert `QuickTest.fs`

---------

Co-authored-by: Kevin Frey <[email protected]>
  • Loading branch information
MangelMaxime and Freymaurer authored Jan 26, 2025
1 parent 7c2e345 commit 5573927
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 36 deletions.
5 changes: 5 additions & 0 deletions src/Fable.Cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added

* [TS] Include XML Doc comment on interface properties (by @Freymaurer)
* [TS] Generate `interface` type when using the "ParamObject" class pattern (by @MangelMaxime)

## 5.0.0-alpha.7 - 2025-01-23

### Fixed
Expand Down
5 changes: 5 additions & 0 deletions src/Fable.Compiler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added

* [TS] Include XML Doc comment on interface properties (by @Freymaurer)
* [TS] Generate `interface` type when using the "ParamObject" class pattern (by @MangelMaxime)

## 5.0.0-alpha.7 - 2025-01-23

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion src/Fable.Transforms/BabelPrinter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,7 @@ module PrinterExtensions =
printer.PrintFunction(Some id, parameters, body, typeParameters, returnType, loc, isDeclaration = true)

printer.PrintNewLine()
| InterfaceDeclaration(id, body, extends, typeParameters) ->
| InterfaceDeclaration(id, body, extends, typeParameters, _) ->
printer.PrintInterfaceDeclaration(id, body, extends, typeParameters)
| EnumDeclaration(name, cases, isConst) ->
if isConst then
Expand Down
67 changes: 63 additions & 4 deletions src/Fable.Transforms/FSharp2Fable.Util.fs
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,50 @@ module Helpers =
| None -> false
)

// When compiling to TypeScript, we want to captuer classes that use the
// [<Global>] attribute on the type and[<ParamObject>] on the constructors
// so we can transform it into an interface

/// <summary>
/// Check if the entity is decorated with the <code>Global</code> attribute
/// and all its constructors are decorated with <code>ParamObject</code> attribute.
///
/// This is used to identify classes that should be transformed into interfaces.
/// </summary>
/// <param name="entity"></param>
/// <returns>
/// <code>true</code> if the entity is a global type with all constructors as param objects,
/// <code>false</code> otherwise.
/// </returns>
let isParamObjectClassPattern (entity: Fable.Entity) =
let isGlobalType =
entity.Attributes |> Seq.exists (fun att -> att.Entity.FullName = Atts.global_)

let areAllConstructorsParamObject =
entity.MembersFunctionsAndValues
|> Seq.filter _.IsConstructor
|> Seq.forall (fun memb ->
// Empty constructors are considered valid as it allows to simplify unwraping
// complex Union types
//
// [<AllowNullLiteral>]
// [<Global>]
// type ClassWithUnion private () =
// [<ParamObjectAttribute; Emit("$0")>]
// new (stringOrNumber : string) = ClassWithUnion()
// [<ParamObjectAttribute; Emit("$0")>]
// new (stringOrNumber : int) = ClassWithUnion()
//
// Without this trick when we have a lot of U2, U3, etc. to map it is really difficult
// or verbose to craft the correct F# class. By using, an empty constructor we can
// "bypass" the F# type system.
memb.CurriedParameterGroups |> List.concat |> List.isEmpty
|| memb.Attributes
|> Seq.exists (fun att -> att.Entity.FullName = Atts.paramObject)
)

isGlobalType && areAllConstructorsParamObject

let tryPickAttrib attFullNames (attributes: FSharpAttribute seq) =
let attFullNames = Map attFullNames

Expand Down Expand Up @@ -2002,10 +2046,25 @@ module Util =

let tryGlobalOrImportedAttributes (com: Compiler) (entRef: Fable.EntityRef) (attributes: Fable.Attribute seq) =
let globalRef customName =
defaultArg customName entRef.DisplayName
|> makeTypedIdent Fable.Any
|> Fable.IdentExpr
|> Some
let name =
// Custom name has precedence
match customName with
| Some name -> name
| None ->
let entity = com.GetEntity(entRef)

// If we are generating TypeScript, and the entity is an object class pattern
// we need to use the compiled name, replacing '`' with '$' to mimic
// how Fable generates the compiled name for generic types
// I was not able to find where this is done in Fable, so I am doing it manually here
if com.Options.Language = TypeScript && isParamObjectClassPattern entity then
entity.CompiledName.Replace("`", "$")
// Otherwise, we use the display name as `Global` is often used to describe external API
// and we want to keep the original name
else
entRef.DisplayName

name |> makeTypedIdent Fable.Any |> Fable.IdentExpr |> Some

match attributes with
| _ when entRef.FullName.StartsWith("Fable.Core.JS.", StringComparison.Ordinal) -> globalRef None
Expand Down
3 changes: 2 additions & 1 deletion src/Fable.Transforms/FSharp2Fable.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2044,7 +2044,8 @@ let rec private transformDeclarations (com: FableCompiler) ctx fsDecls =

if
(isErasedOrStringEnumEntity ent && Compiler.Language <> TypeScript)
|| isGlobalOrImportedEntity ent
|| (isGlobalOrImportedEntity ent
&& (not (Compiler.Language = TypeScript && isParamObjectClassPattern ent)))
then
[]
else
Expand Down
142 changes: 116 additions & 26 deletions src/Fable.Transforms/Fable2Babel.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3782,6 +3782,89 @@ module Util =

declareType com ctx ent entName args body baseExpr classMembers

let transformParamObjectClassPatternToInterface
(com: IBabelCompiler)
ctx
(classDecl: Fable.ClassDecl)
(ent: Fable.Entity)
=

let constructors =
ent.MembersFunctionsAndValues
|> Seq.filter _.IsConstructor
|> Seq.choose (fun constructor ->
let parameters = List.concat constructor.CurriedParameterGroups

if parameters.Length = 0 then
None
else

parameters
|> List.mapi (fun index arg ->
let name = defaultArg arg.Name $"arg{index}"

/// Try to find getter/setter in f# syntax for POJOs. If found propagate its xml doc to interface.
let tryXmlDoc =
ent.MembersFunctionsAndValues
|> Seq.tryFind (fun s -> s.DisplayName = name)
|> Option.bind (fun tgs -> tgs.XmlDoc)

let typeAnnotation =
if arg.IsOptional then
unwrapOptionalType arg.Type
else
arg.Type
|> FableTransforms.uncurryType
|> makeTypeAnnotation com ctx

AbstractMember.abstractProperty (
name |> Identifier.identifier |> Expression.Identifier,
typeAnnotation,
isOptional = arg.IsOptional,
?doc = tryXmlDoc
)
)
|> Array.ofSeq
|> Some
)
|> Seq.toList


let typeParameters =
ent.GenericParameters
|> List.map (fun g -> Fable.GenericParam(g.Name, g.IsMeasure, g.Constraints))
|> makeTypeParamDecl com ctx

match constructors with
| [] ->
addError
com
[]
None
"Unable to find a valid constructor for generating interface via ParamObject, please make sure the constructor has at least one parameter."

[]
| members :: [] ->
Declaration.interfaceDeclaration (
Identifier.identifier classDecl.Name,
members,
[||],
typeParameters,
?doc = classDecl.XmlDoc
)
|> asModuleDeclaration ent.IsPublic
|> List.singleton
| _ ->
let typ =
List.map ObjectTypeAnnotation constructors
|> Array.ofList
|> UnionTypeAnnotation

TypeAliasDeclaration(classDecl.Name, typeParameters, typ)
|> asModuleDeclaration ent.IsPublic
|> List.singleton


let transformClassWithPrimaryConstructor
(com: IBabelCompiler)
ctx
Expand Down Expand Up @@ -4124,33 +4207,40 @@ module Util =
else
[]
| ent ->
let classMembers =
decl.AttachedMembers
|> List.toArray
|> Array.collect (fun memb ->
withCurrentScope ctx memb.UsedNames
<| fun ctx ->
memb.ImplementedSignatureRef
|> Option.bind (com.TryGetMember)
|> Option.orElseWith (fun () -> com.TryGetMember(memb.MemberRef))
|> function
| None -> [||]
| Some info ->
if not memb.IsMangled && (info.IsGetter || info.IsSetter) then
transformAttachedProperty com ctx ent info memb
else
transformAttachedMethod com ctx ent info memb
)
if
Compiler.Language = TypeScript
&& FSharp2Fable.Helpers.isParamObjectClassPattern ent
then
transformParamObjectClassPatternToInterface com ctx decl ent
else

match decl.Constructor with
| Some cons ->
withCurrentScope ctx cons.UsedNames
<| fun ctx -> transformClassWithPrimaryConstructor com ctx ent decl classMembers cons
| None ->
if ent.IsFSharpUnion then
transformUnion com ctx ent decl.Name classMembers
else
transformClassWithCompilerGeneratedConstructor com ctx ent decl.Name classMembers
let classMembers =
decl.AttachedMembers
|> List.toArray
|> Array.collect (fun memb ->
withCurrentScope ctx memb.UsedNames
<| fun ctx ->
memb.ImplementedSignatureRef
|> Option.bind (com.TryGetMember)
|> Option.orElseWith (fun () -> com.TryGetMember(memb.MemberRef))
|> function
| None -> [||]
| Some info ->
if not memb.IsMangled && (info.IsGetter || info.IsSetter) then
transformAttachedProperty com ctx ent info memb
else
transformAttachedMethod com ctx ent info memb
)

match decl.Constructor with
| Some cons ->
withCurrentScope ctx cons.UsedNames
<| fun ctx -> transformClassWithPrimaryConstructor com ctx ent decl classMembers cons
| None ->
if ent.IsFSharpUnion then
transformUnion com ctx ent decl.Name classMembers
else
transformClassWithCompilerGeneratedConstructor com ctx ent decl.Name classMembers

let transformImports (imports: Import seq) : ModuleDeclaration list =
let statefulImports = ResizeArray()
Expand Down
10 changes: 6 additions & 4 deletions src/Fable.Transforms/Global/Babel.fs
Original file line number Diff line number Diff line change
Expand Up @@ -188,14 +188,16 @@ type Declaration =
id: Identifier *
members: AbstractMember array *
extends: TypeAnnotation array *
typeParameters: TypeParameter array
typeParameters: TypeParameter array *
doc: string option
| EnumDeclaration of name: string * cases: (string * Expression) array * isConst: bool
| TypeAliasDeclaration of name: string * typeParameters: TypeParameter array * alias: TypeAnnotation

member this.JsDoc =
match this with
| ClassDeclaration(_, _, _, _, _, _, doc)
| FunctionDeclaration(_, _, _, _, _, _, doc) -> doc
| FunctionDeclaration(_, _, _, _, _, _, doc)
| InterfaceDeclaration(_, _, _, _, doc) -> doc
| _ -> None

/// A module import or export declaration.
Expand Down Expand Up @@ -779,8 +781,8 @@ module Helpers =
static member classDeclaration(body, ?id, ?superClass, ?typeParameters, ?implements, ?loc, ?doc) =
ClassDeclaration(body, id, superClass, defaultArg implements [||], defaultArg typeParameters [||], loc, doc)

static member interfaceDeclaration(id, body, ?extends, ?typeParameters) : Declaration = // ?mixins_,
InterfaceDeclaration(id, body, defaultArg extends [||], defaultArg typeParameters [||])
static member interfaceDeclaration(id, body, ?extends, ?typeParameters, ?doc) : Declaration = // ?mixins_,
InterfaceDeclaration(id, body, defaultArg extends [||], defaultArg typeParameters [||], doc)

static member enumDeclaration(name, cases, ?isConst) =
EnumDeclaration(name, cases, defaultArg isConst false)
Expand Down
Loading

0 comments on commit 5573927

Please sign in to comment.