ocsigen / ts2ocaml

Generate OCaml bindings from TypeScript definitions via the TypeScript compiler API

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support constructor API

tmattio opened this issue · comments

For instance in lib.es2015.promise.d.ts:

interface PromiseConstructor {
    readonly prototype: Promise<any>;

    new <T>(executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;

    ...
}

declare var Promise: PromiseConstructor;

The global namespace Promise is assigned to PromiseConstructor. However, in the generated file, this translates to val promise: _PromiseConstructor [@@js.global "Promise"]. It should generate a module with the global values instead. Example:

module Promise : sig
  include module type of struct
    include Promise
  end

  val create
    :  (resolve:('T -> unit) -> reject:('E -> unit) -> unit)
    -> 'T Promise.t
    [@@js.new "Promise"]
end

I'm wondering whether it's good or bad not to strictly follow the definitions in the original .d.ts (although this "constructor pattern" is used very frequently).

If we keep the PromiseConstructor module, adding the create and other global functions to Promise does actually follow the d.ts, since Promise is extended by the PromiseConstructor interface with declare var Promise: PromiseConstructor;. So I would say technically, we're not following the spec at the moment. For instance, in ts, this is possible:

Promise.create((resolve, reject) => ...)

But not with the generated bindings, where this would look like:

let ctor = PromiseConstructor.create Promise.promise (fun ~resolve ~reject -> ...)

But more generally, to give my two cents on your question, I'd like the bindings to be as close as possible to the spec. If there is a need for more opinionated changes, users can always create a layer of abstraction.

I added two options --simplify-immediate-instance and --simplify-immediate-constructor.

The former simplifies the following pattern

interface Foo = {
  someMethod(value: number): void;
  ...
}
declare var Foo: Foo;

to

module [@js.scope "Foo"] Foo : sig
  val someMethod: float -> unit [@js.global "someMethod"]
  ...
end

and the latter simplifies the following pattern:

interface Foo {
  someMethod(value: number): void;
  ...
}
interface FooConstructor {
  new(...): Foo;
  someStaticMethod(value: number): void;
  ...
}
declare var Foo: FooConstructor;

to

module [@js.scope "Foo"] Foo : sig
  type t = ...
  val someMethod: t -> float -> unit [@js.call "someMethod"]
  ...

  val create: ... -> t [@@js.create]
  val someStaticMethod: float -> unit [@js.global "someStaticMethod"]
  ...
end