Closed bilkusg closed 8 years ago
Yes, this is a concept that it's difficult to translate from TypeScript, as TS happily accepts instances with the optional members missing but in F# you need to instantiate them all even if the value is None
(that's why I'm not using records to translate them: the constructor would be too verbose).
Currently there are two ways to deal with this. One is to create an empty object and assign the properties you want:
let sqlconfig = createEmpty<IConnectionConfig>
sqlconfig.host <- "10.29.5.207"
As these interfaces are usually used to pass config options to a JS library, the other alternative is to use union types with KeyValueList
attribute as in the React example. I have a script to build these union types from a definition file if you need to write them. We could consider to do it automatically when parsing TS definition files, but I'm not sure what criteria should decide which interfaces are used for configuration.
Example of using KeyValueList
union types, the list will be compiled as a JS object:
https://github.com/fsprojects/Fable/blob/master/samples/browser/react/src/client/components.fs#L65-L72
Thanks for that. The createEmpty approach looks reasonable enough - your example is slightly wrong in that I had to write
let sqlconfig = createEmpty<IConnectionConfig>
sqlconfig.host <- Some "10.29.5.207"
But it supported intellisense, and generated the right code
What would be nice would be a comment in the generated file referring to this approach, for the benefit of future users.
But I do prefer the KeyValueList syntax for this, and I suspect others will too. Is the problem that you can't generate both solutions in the same namespace and there's no obvious way to include both? Otherwise, just create both and invite people to comment out the one they don't need, or give them different names and leave them both in.
Incidentally, I think that Fable's ability to generate working interfaces for lots of existing javascript libraries by leveraging typescript files is one of its key selling points, and I think it's worth focusing on making it as easy and surprise free as possible.
On the same basic topic, can I throw in a couple more questions:
default
allowed me to call them dynamically, and I could easily bind them manually if I had to, but is this just a missing feature or another subtle semantic challenge?The source file relevant bit is below:
# All this stuff at the top of the file is ignored
declare function moment(): moment.Moment;
declare function moment(date: number): moment.Moment;
declare function moment(date: number[]): moment.Moment;
declare function moment(date: string, format?: string, strict?: boolean): moment.Moment;
declare function moment(date: string, format?: string, language?: string, strict?: boolean): moment.Moment;
declare function moment(date: string, formats: string[], strict?: boolean): moment.Moment;
declare function moment(date: string, formats: string[], language?: string, strict?: boolean): moment.Moment;
declare function moment(date: string, specialFormat: () => void, strict?: boolean): moment.Moment;
declare function moment(date: string, specialFormat: () => void, language?: string, strict?: boolean): moment.Moment;
declare function moment(date: string, formatsIncludingSpecial: any[], strict?: boolean): moment.Moment;
declare function moment(date: string, formatsIncludingSpecial: any[], language?: string, strict?: boolean): moment.Moment;
declare function moment(date: Date): moment.Moment;
declare function moment(date: moment.Moment): moment.Moment;
declare function moment(date: Object): moment.Moment;
# From here on everything is as expected
declare namespace moment {
type formatFunction = () => string;
interface MomentDateObject {
years?: number;
/* One digit */
months?: number;
Yes, I totally agree that typed interfaces to interact with JS libraries is a tremendously helpful feature (it's actually the main selling point of TypeScript) and being able to reuse the work of Typescript type definitions saves a lot of time. But d.ts files can get really complicated (TypeScript keeps adding new syntax with every release) and while the TypeScript Compiler Services API is very powerful, it's not well documented and not completely stable. As Fable's user base is now expanding (which makes really, really happy) I'm trying to deal quickly with the bugs and it's difficult to focus on the rewrite that the ts2fable
parser needs. Anyways, I'll try to get in touch with the TypeScript team, they've been very helpful in the past and may provide some guidance.
Regarding your other questions:
Is there syntax to generate a different name for a member in javascript than the name it's given in F#?
The F# compiler distinguishes between DisplayName
and CompiledName
, I was thinking to use the last but at the end I'm using the former to prevent confusions (e.g. the compiled name of properties start with get_
). However we can consider it for some cases (like interfaces). Actually, at the moment you can use the CompiledNameAttribute to alter the compilation of StringEnum and KeyValueList.
The other trick you can use right now is the Emit
attribute:
type MyInterface =
[<Emit("$0.otherName($1...)"> abstract member myMethod: int * int -> int
And I notice on importing moment.d.ts that the functions built directly on the moment object don't get anything generated for them. I found that using moment.default allowed me to call them dynamically, and I could easily bind them manually if I had to, but is this just a missing feature or another subtle semantic challenge?
You can consider it a missing feature. TS definitions can be used for globally accessible functions or for import/export modules. The first definitions were mainly of the first class but I assumed now most will follow the ES6 module system, that's why I was ignoring functions outside a module.
We can translate them to F# though it will be a bit tricky as you cannot put functions directly in a namespace. A dummy type must be created for that.
Thanks for the above. One further thought. Because F# types which are interfaces can't have non-abstract members, there's no obvious place for me to put my async bindings for the mysql library.
mysql defines an IConnection interface, and what I'd like to be able to do is extend it with methods which are defined entirely in terms of existing IConnection abstract methods. But I can't. So I either need to create standalone functions which take an IConnection parameter, or I need to create a separate new class which boringly delegates all the other functions to an embedded IConnection object.
Not hard to do, but messy. The alternative would be perhaps a way of making these wrappers look like concrete classes which happen to emit javascript as their implementations. Then I could just inherit or add concrete methods without retyping everything.
I'm not sure I understand you, could you please post some code to illustrate what you're trying to do? About interfaces and non-abstract members, have you tried Abstract classes?
OK. So the autogenerated mysql file has a bit which looks like this ( only a few functions retained ):
...
and IConnection =
abstract query: IQueryFunction with get, set
abstract connect: unit -> unit
abstract ``end``: unit -> unit
and IQueryFunction =
[<Emit("$0($1...)")>] abstract Invoke: sql: string * values: obj * callback: Func<IError, obj, unit> -> IQuery
type [<Import("*","mysql")>] Globals =
static member createConnection(connectionUri: string): IConnection = failwith "JS only"
which allows me to write code like this
let connection = mysql.Globals.createConnection(connectionConfig)
connection.connect()
connection.query.Invoke("select * from table1",Func<IError,obj,unit> callback )
// where callback is a callback function defined somewhere
Based on the earlier discussion in this thread, and the now fixed async library, I have written some helper functions:
let queryAsync (connection:IConnection) (queryString: string) =
Async.FromContinuations (fun(r,e,x)->
connection.query.Invoke(queryString,fun (err:IError option ) result->
match err with
| None -> r result
| Some errorFound -> e (new Exception(errorFound.message))
) |> ignore
)
and I can now write
let connection = mysql.Globals.createConnection(connectionConfig)
connection.connect()
async {
let! queryResult = mysql.queryAsync connection ("select * from table1")
//...
} |> StartImmediate
which is fine, and I can live with it. But for consistency, I'd like my queryAsync function to be a member of connection so I could say
let! queryResult = connection.queryAsync("select * from table1")
But although logically connection is a javascript object which could have additional methods added to it, it comes wrapped in an abstract interface which can't have a concrete method added to it.
If instead, it were possible to have defined the IConnection interface as a class with concrete dummy methods like Globals, then I could easily just add the extra methods to the class definition.
I guess this can be done manually using some combination of Import and Emit, but I'm not sure I understand the consequences of trying this, or what best practice should be, and I think I'd like ts2fable to generate (optionally) a concrete class like Globals rather than an abstract interface, for what is, in fact, a concrete class in javascript.
Hope this at least clarifies my question.
Ok, I understand much better what you need, thank you! There was already a similar discussion to this but because of the semantic nuances and problems with inheritance, etc. I decided to keep it simple at the end. Again, parsing TypeScript files is really complicated and the more features we add the more difficult it'll be to maintain the parser. The idea of generating simple source code was precisely to allow the user easy editing if necessary.
There's also another problem. Currently the compiler is ignoring any file starting with Fable.Import
so if you want to actually generate code from the definition you have to rename the file or put it in a different place (check the helper file in the React package for an example). In your case, as you want to attach the methods to the interface I think the best solution is to create a type extension in a helper file:
namespace Fable.Helpers
[<AutoOpen>]
module mysql_Extensions =
open Fable.Import.Mysql
type IConnection with
member x.queryAsync (queryString: string) =
Async.FromContinuations (fun(r,e,x)->
connection.query.Invoke(queryString,fun (err:IError option ) result->
match err with
| None -> r result
| Some errorFound -> e (new Exception(errorFound.message))
) |> ignore
)
OK thanks. I can certainly live with the type extension option if that's the most future-proof way of doing something like this. It does certainly allow me to consume the result in a consistent way, which is my main objective.
Description
When I use ts2fable to import the node-mysql library, I get lots of interfaces like this:
which are used as parameters to various calls. I'm not sure what best practice is for actually creating something which can be used where this interface is expected. . Do I
and thus lose all the benefit of type-safety
Advice please!