Generates binding for js_of_ocaml
.
ts2ocaml
is a powerful tool, but there are so many options and also some caverts.
Therefore, we first provide a walkthrough to use this tool for your project.
The documentation for the ts2ocaml
command and its options comes after the walkthrough, starting with the Usage setion.
ts2ocaml
for js_of_ocaml
generates .mli
files, which should then be processed with LexiFi/gen_js_api
.
You should use the latest gen_js_api
as ts2ocaml
uses the latest features of gen_js_api
.
As of Oct 2021, most of the required features have not been present in the latest version in opam.
So you would have to either do
opam pin add gen_js_api https://github.com/LexiFi/gen_js_api.git
(recommended), orgit submodule
their repository to thelib
directory of your OCaml project.- Note that if you use
gen_js_api
via a submodule, it might conflict withts2ocaml-jsoo-stdlib
which is installed viaocaml pin add
. - Therefore, this would work only if you are going to do
ts2ocaml jsoo --create-minimal-lib
.
- Note that if you use
Any bindings to JS packages, not limited to the ones generated by ts2ocaml
, need a standard library to compile and run, which mainly consists of the bindings to JS and DOM APIs.
Actually, ts2ocaml
is so powerful that it is capable of generating the standard library for itself.
However, the resulting code is rather big (~20MB in .ml
, ~40MB in .cma
), so letting users to generate it themselves and add it to their project is not really a good option. Most users would want to use an OPAM package instead.
Also, we understand not everyone wants to install such a big library, especially when they already have their preferred bindings for JS and DOM APIs. Such users would only want a minimal standard library.
To fulfill both needs, we've made two ways to add the standard library.
This package contains the full bindings for JS, DOM, and Web Worker API, generated with the full
preset.
As described in Requirements, ts2ocaml
needs the latest gen_js_api
, which is still not present in OPAM repository.
So, ts2ocaml-jsoo-stdlib
is currently not in OPAM repository.
To install it to your OPAM switch, we recommend you to use opam pin
.
The standard library for ts2ocaml
version X.Y.Z
is available as the jsoo-stdlib-vX.Y.Z
tag in this repository.
Check the version of ts2ocaml
with ts2ocaml --version
and do the following:
opam pin add ts2ocaml-jsoo-stdlib https://github.com/ocsigen/ts2ocaml.git#jsoo-stdlib-vX.Y.Z
Using --create-minimal-lib
ts2ocaml jsoo --create-minimal-lib
generates a minimal standard library for ts2ocaml
.
It only contains the following definitions:
-'tags intf
type, which is used for tag-based subtyping.- TypeScript-specific primitive types, such as
any
,never
,unknown
, etc. - Utility types for handling TypeScript's union types and intersection types.
You can safely add it to your project, and even modify it for your needs.
Note that you have to modify the bindings generated by ts2ocaml
to make it work with the minimal standard library.
- Remove the
open Ts2ocaml
statements and then addopen Ts2ocaml_min
(or your own name if you renamed it). - All the JS, DOM, and Web Worker types are left unknown. You have to replace them with the types from your binding by hand.
- We recommend using
--preset=minimal
when generating bindings, which disables all the features irrelevant to the bindings not fromts2ocaml
.
- We recommend using
ts2ocaml
has many options, so there is an option --preset
to set multiple options at once which is commonly used together.
--preset=minimal
- A preset to minimize the output.
- Intended for library authors, who will modify the output and build a binding library upon it.
- It generates the simplest binding.
- However, it lacks subtyping and it will not compile if the package depends on another package.
--preset=safe
- A preset to generate a code which just compiles and works.
- Suited for generating bindings for relatively small packages, which involve less inheritance and slightly depend on other packages.
- e.g.
yargs
, which has a minimal dependency and does not make use ofextends
so much.
- e.g.
--preset=full
- A preset to generate a code with more type safety and more support for package dependency.
- Suited for generating bindings for large packages, which have many
extends
and/or heavily depend on another package.- e.g. React component packages, which almost certainly inherits many interfaces from React.
--preset
doesn't override options you explicitly set.
See --preset
for the options which will be set by each preset.
Hint: if a package
foo
depends only onbar
andbar
depends on many other packages, it's safe to use--preset=safe
tofoo
and--preset=full
tobar
, but not vice versa.
Once you figure out which preset (and some additional options if any) to use, you are now ready to run ts2ocaml
.
ts2ocaml jsoo --preset full --output-dir src node_modules/typescript/lib/typescript.d.ts
A binding (typescript.mli
in this example) and a JS stub file stub.js
will be generated in the src
directory.
Modify your dune
file to include them in your project.
- Use
(js_of_ocaml (javascript_files stub.js))
to include the stub JS to the executable. - Add a rule to run
gen_js_api
on the generated.mli
file.
The resulting dune
will look like below:
(executable
(name your_app)
(libraries gen_js_api ts2ocaml-jsoo-stdlib)
(link_flags -no-check-prims)
(preprocess (pps gen_js_api.ppx))
(modes js)
(js_of_ocaml
(javascript_files stub.js)))
(rule
(targets typescript.ml)
(deps typescript.mli)
(action (run %{bin:gen_js_api} %{deps})))
(rule
(targets main.js)
(deps main.bc.js)
(action (copy %{deps} %{targets})))
(alias
(name DEFAULT)
(deps main.js main.html))
Note: if you are using
ts2ocaml-jsoo-stdlib
, don't forget to set--profile=release
when running dune! There is no dead code elimination without that option, so it would result in a 40MB+ JavaScript executable.
Each binding has an Export
module which corresponds to the package's default export (export default ..
or export = ..
in TypeScript).
Define a module alias to "import" the package:
module TypeScript = Typescript.Export
let () =
Printf.printf "typescript version: %s\n" (TypeScript.version ())
;;
See also the documentation of:
To work with multiple files and packages, ts2ocaml has some conventions around the name of the generated OCaml source codes.
- If not known, ts2ocaml computes the JS module name of the input
.d.ts
file by heuristics. - ts2ocaml converts the JS module name to a OCaml module name by the followings:
- Removes
@
at the top of the module name - Replaces
/
with__
- Replaces any other signs (such as
-
) to_
- Removes
- ts2ocaml uses the OCaml module name as the output file name.
- If the filename is equal to
types
ortypings
ofpackage.json
, then ts2ocaml will use the package name itself.- input:
node_modules/typescript/lib/typescript.d.ts
package.json
:"typings": "./lib/typescript.d.ts",
getJsModuleName
:typescript
- output file:
typescript.mli
- input:
- If the filename is present in
exports
ofpackage.json
, then ts2ocaml will combine the package name and the exported module name.- input:
node_modules/@angular/common/http/http.d.ts
package.json
:"exports": { .., "./http": { "types": "./http/http.d.ts", .. }, .. }
getJsModuleName
:@angualr/common/http
- output file:
angular__common__http.mli
- input:
- Otherwise, ts2ocaml uses a heuristic module name: it will combine the package name and the filename.
index.d.ts
is handled specially.- input:
node_modules/cassandra-driver/lib/auth/index.d.ts
getJsModuleName
:cassandra-driver/auth
- output file:
cassandra_driver__auth.mli
- if
package.json
is not present, the package name is also inferred heuristically from the filename.
- input:
import
of another package fromnode_modules
will be converted to anopen
statement or a module alias.- The OCaml module name of the imported package is computed by the step 2 of the above.
// node_modules/@types/react/index.d.ts
import * as CSS from 'csstype';
import { Interaction as SchedulerInteraction } from 'scheduler/tracing';
...
(* react.mli *)
module CSS = Csstype.Export
module SchedulerInteraction = Scheduler__tracing.Export.Interaction
...
import
of relative path will be converted to anopen
statement or a module alias.- The OCaml module name of the imported file will also be inferred by heuristics.
// node_modules/cassandra-driver/index.d.ts
import { auth } from './lib/auth';
(* cassandra_driver.mli *)
module Auth = Cassandra_driver__auth.Export.Auth
// node_modules/cassandra-driver/lib/mapping/index.d.ts
import { Client } from '../../';
(* cassandra_driver__mapping.mli *)
module Client = Cassandra_driver.Export.Client
- Indirect
import
using identifiers is not yet be supported.
import { types } from './lib/types';
import Uuid = types.Uuid; // we should be able to convert this to `module Uuid = Type.Uuid`, but not yet
- Direct
export
of an external module will not be supported.
export { someFunction } from './lib/functions'; // this is VERY hard to do in OCaml!
ts2ocaml will create a module named Export
to represent the exported definitions.
- If an export assignment
export = Something
is used, theExport
module will be an alias to theSomething
module.
(* export = Something *)
module Export = Something
- If ES6 exports
export interface Foo
orexport { Bar }
are used, theExport
module will contain the exported modules.
module Export : sig
(* export interface Foo *)
module Foo = Foo
(* export { Bar } *)
module Bar = Bar
(* export { Baz as Buzz } *)
module Buzz = Baz
end
This is why you are advised to use the generated bindings with the following:
(* This is analogous to `import * as TypeScript from "typescript";` *)
module TypeScript = Typescript.Export
$ ts2ocaml jsoo [options] <inputs..>
When multiple input files are given, ts2ocaml
will merge it to one source file.
ts2ocaml
will not resolve imports and exports between input files, so it can result in a broken binding.
We recommend you to create one binding for each .d.ts
. It works in most of the cases, and it is easy to fix when the binding is broken.
See also the common options.
Specify the preset to use.
--preset=minimal
- It sets
--simplify=all
and--rec-module=optimized
.
- It sets
--preset=safe
- It sets
--safe-arity=full
and--subtyping=cast-function
. - It also sets all the options
--preset=minimal
sets.
- It sets
--preset=full
- It sets
--inherit-with-tags=full
and--subtyping=tag
. - It also sets all the options
--preset=safe
sets.
- It sets
Create ts2ocaml_min.mli
, which is the minimal standard library for ts2ocaml without any bindings for
- JS standard API (
Ts2ocaml_es
), - DOM API (
Ts2ocaml_dom
), or - web worker API (
Ts2ocaml_webworker
).
When this option is used, ts2ocaml requires no input files. So most of the other options will be ignored.
The directory to place the generated bindings. If not set, it will be the current directory.
The name of the JS stub file to import/require JS modules.
If not set, it will be stub.js
.
If the stub file already exists, ts2ocaml
will append new entries.
The resulting stub.js
will look like:
joo_global_object["React"] = require('react')
joo_global_object["ReactModal"] = require('react-modal')
joo_global_object["prop-types"] = require('prop-types') /* need Babel */
joo_global_object["scheduler/tracing"] = require('scheduler/tracing') /* need Babel */
joo_global_object["ts"] = require('typescript')
joo_global_object["yargs"] = require('yargs')
joo_global_object["yargsParser"] = require('yargs-parser')
The stub file uses require
for importing packages. /* need Babel */
indicates the referenced package is actually a ES6 module and so it needs to be converted by Babel.
Treat number types as int
. If not set, float
will be used.
See also the detailed docs about modeling TypeScript's subtyping in OCaml.
Turn on subtyping features.
You can use --subtyping=foo,bar
to turn on multiple features. Also, use --subtyping=off
to explicitly disable subtyping features.
Use -'tags intf
for class and interface types, which simulates nominal subtyping by putting to 'tags
the class names as a polymorphic variant.
For example, assume we have the following input:
interface A { methA(a: number): number; }
interface B extends A { methB(a: number, b: number): number; }
interface C extends B { methC(a: number, b: number, c: number): number; }
When this feature is used, the resulting binding will look like:
module A : sig
type t = [ `A ] intf
val methA: t -> a:float -> float
val cast_from: [> `A] intf -> t
end
module B : sig
type t = [ `B | `A ] intf
val methB: t -> a:float -> b:float -> float
val cast_from: [> `B] intf -> t
end
module C : sig
type t = [ `C | `B | `A ] intf
val methC: t -> a:float -> b:float -> c:float -> float
val cast_from: [> `C] intf -> t
end
So if we have a val x : C.t
, you can directly cast it to A.t
by writing x :> A.t
.
Alternatively, you can also write A.cast_from x
, which uses a generic cast function cast_from
.
let c : C.t = ...
let a1 : A.t = c :> A.t
let a2 : A.t = A.cast_from c
Add cast
functions to cast types around.
For example, assume we have the following input:
interface A { methA(a: number): number; }
interface B extends A { methB(a: number, b: number): number; }
interface C extends B { methC(a: number, b: number, c: number): number; }
When this feature is used, the resulting binding will look like:
module A : sig
type t
val methA: t -> a:float -> float
end
module B : sig
type t
val methB: t -> a:float -> b:float -> float
val cast_to_A: t -> A.t
end
module C : sig
type t
val methC: t -> a:float -> b:float -> c:float -> float
val cast_to_B: t -> B.t
end
So if we have a val x : C.t
, you can cast it to A.t
by writing B.cast_to_A (C.cast_to_B x)
.
let c : C.t = ...
let a : A.t = x |> C.cast_to_B |> B.cast_to_A
This feature is less powerful than tag
, but it has some use cases tag
doesn't cover.
tag
doesn't support diamond inheritance, whilecast-function
does.- When
--inherit-with-tags
is not used,tag
doesn't support casting a type to other from a different package, whilecast-function
does.
Note: This options requires
--subtyping=tag
. If thetag
feature is not specified, it will fail with an error.
Use TypeName.tags
type names to inherit types from other packages.
--inherit-with-tags=full
(default)- It generates
tags
types in the module, and tries to usetags
type to inherit a type if it is unknown (e.g. from another package).
- It generates
--inherit-with-tags=provide
- It only generates
tags
types in the module.
- It only generates
--inherit-with-tags=consume
- It only tries to use
tags
type if the inherited type is unknown.
- It only tries to use
--inherit-with-tags=off
- It disables any usage of
tags
types.
- It disables any usage of
For example, assume we have node_modules/foo/index.d.ts
and node_modules/bar/index.d.ts
as the following:
// foo/index.d.ts
declare namespace foo {
interface A { ... }
}
export = foo;
// bar/index.d.ts
import * as Foo from 'foo';
declare namespace bar {
interface B extends A { ... }
}
export = bar;
Then the outputs will look like depending on the option you set:
(* Foo.mli *)
module Foo : sig
module A : sig
type t = [`A] intf
(* this will be generated if `full` or `provide` is set *)
type tags = [`A]
(* this will be generated regardless of the option *)
val cast_from: [> `A] intf -> t
...
end
end
(* export = foo; *)
module Export = Foo
(* Bar.mli *)
(* import * as Foo from "foo"; *)
module Foo = Foo.Export
module Bar : sig
module B : sig
(* if `full` or `consume` is set, this will be generated *)
type t = [`B | Foo.A.tags] intf
(* otherwise, this will be generated *)
type t = [`B] intf
(* if `full` is set, this will be generated *)
type tags = [`B | Foo.A.tags]
(* else if `provide` is set, this will be generated *)
type tags = [`B]
(* this will be generated regardless of the option *)
val cast_from: [> `B] intf -> t
...
end
end
(* export = bar; *)
module Export = Bar
If provide
or full
is used for foo.d.ts
and consume
or full
is used for bar.d.ts
,
you will be able to safely cast B.t
to A.t
, although they come from different packages.
module Foo = Foo.Export
module Bar = Bar.Export
let bar : Bar.B.t = ...
let foo1 : Foo.A.t = bar :> Foo.A.t
let foo2 : Foo.A.t = Foo.A.cast_from bar
Otherwise, you can't safely cast B.t
to A.t
. To do it, you will have to
- set
--subtyping=cast-function
to obtainval cast_to_A: t -> A.t
, or - manually add
`A
to the definition ofB.t
(andB.tags
if you choose to provide).
Note:
TypeName.tags
types will come with the "arity-safe" version of them if--safe-arity
is also set.
Use TypeName.t_n
type names to safely use overloaded types from other packages.
--safe-arity=full
- It generates
t_n
types in the module, and tries to uset_n
type if the type is unknown (e.g. from another package).
- It generates
--safe-arity=provide
- It only generates
t_n
types in the module.
- It only generates
--safe-arity=consume
- It only tries to use
t_n
type if the type is unknown.
- It only tries to use
--safe-arity=off
(default)- It disables any usage of
t_n
types.
- It disables any usage of
For example, assume we have node_modules/foo/index.d.ts
and node_modules/bar/index.d.ts
as the following:
// foo/index.d.ts
declare namespace foo {
interface A<T> { ... }
interface B<T = any> { ... }
}
export = foo;
// bar/index.d.ts
import * as Foo from 'foo';
declare function useA(a: Foo.A<T>) : void;
declare function useB(b: Foo.B<T>) : void;
declare function useBDefault(b: Foo.B) : void;
Then the outputs will look like depending on the option you set:
(* Foo.mli *)
module Foo : sig
module A : sig
type 'T t = [`A of 'T] intf
(* this will be generated if `full` or `provide` is set *)
type 'T t_1 = 'T t (* for arity 1 *)
...
end
module B : sig
type 'T t = [`B of 'T] intf
(* this will be generated if `full` or `provide` is set *)
type 'T t_1 = 'T t (* for arity 1 *)
(* this will be generated regardless of the option, since B contains an optional type parameter *)
type t_0 = any t (* for arity 0 *)
...
end
end
(* export = foo; *)
module Export = Foo
(* Bar.mli *)
(* import * as Foo from "foo"; *)
module Foo = Foo.Export
(* if `full` or `consume` is set, this will be generated *)
val useA: 'T Foo.A.t_1 -> unit
val useB: 'T Foo.B.t_1 -> unit
val useBDefault: Foo.B.t_0 -> unit
(* otherwise, this will be generated *)
val useA: 'T Foo.A.t -> unit
val useB: 'T Foo.B.t -> unit
val useBDefault: Foo.B.t -> unit (* this does not compile! *)
Use recursive modules to simplify the output. Can impact the compilation time.
Assume we have the following input:
interface A {
readonly b: B;
}
interface B {
readonly a: A;
}
interface C {
readonly a: A;
readonly b: B;
}
--rec-module=optimized
- It applies strongly-connected-component algorithm to find the smallest sets of recursively defined types and modules.
module rec A : sig
type t = ...
val get_b: unit -> B.t
end
and B : sig
type t = ...
val get_a: unit -> A.t
end
module C : sig
type t = ...
val get_a: unit -> A.t
val get_b: unit -> B.t
end
--rec-module=naive
- It simply makes every module recursive.
- Not very recommended because hundreds of recursive modules would torture OCaml compiler.
module rec A : sig
type t = ...
val get_b: unit -> B.t
end
and B : sig
type t = ...
val get_a: unit -> A.t
end
and C : sig
type t = ...
val get_a: unit -> A.t
val get_b: unit -> B.t
end
--rec-module=off
(default)- It generates types and the corresponding modules (which contain methods and fields for the type) separately.
type _A = ...
type _B = ...
type _C = ...
module A : sig
type t = _A
val get_b: unit -> _B
end
module B : sig
type t = _B
val get_a: unit -> _A
end
module C : sig
type t = _C
val get_a: unit -> _A
val get_b: unit -> _B
end
Turn on simplification features.
You can use --simplify=foo,bar
to turn on multiple features. Also, --simplify=all
enables all the features and --simplify=off
explicitly disables simplification features.
Simplifies a value definition of an interface type with the same name (case sensitive) to a module.
Assume we have the following input:
interface Foo = {
someMethod(value: number): void;
}
declare var Foo: Foo;
If this option is set, the output will be:
module [@js.scope "Foo"] Foo : sig
val someMethod: float -> unit [@@js.global "someMethod"]
end
(* usage *)
let _ = Foo.someMethod 42.0
Otherwise, the output will be:
module Foo : sig
type t = ...
val someMethod: t -> float -> unit [@@js.call "someMethod"]
end
val foo: unit -> Foo.t [@@js.global "Foo"]
(* usage *)
let _ = Foo.someMethod (foo ()) 42.0
A notable example is the Math
object in ES5 (https://github.com/microsoft/TypeScript/blob/main/lib/lib.es5.d.ts).
Simplifies so-called constructor pattern.
Assume we have the following input:
interface Foo = {
someMethod(value: number): void;
}
interface FooConstructor {
new(name: string) : Foo;
anotherMethod(): number;
}
declare var Foo: FooConstructor;
If this option is set, the output will be:
module [@js.scope "Foo"] Foo : sig
type t = ...
val someMethod: t -> float -> unit [@@js.call "someMethod"]
val create: string -> t [@@js.create]
val anotherMethod: unit -> float [@@js.global "anotherMethod"]
end
(* usage *)
let _ =
let foo = Foo.create "foo" in
let num = Foo.anotherMethod () in
Foo.someMethod foo num
Otherwise, the output will be:
module Foo : sig
type t = ...
val someMethod: t -> float -> unit [@js.call "someMethod"]
end
module FooConstructor : sig
type t = ...
val create: t -> name:string -> Foo.t [@@js.apply_newable]
val anotherMethod: t -> unit -> float [@@js.call "anotherMethod"]
end
val foo: unit -> FooConstructor [@@js.global "Foo"]
(* usage *)
let _ =
let foo = FooConstructor.create (foo ()) "foo" in
let num = FooConstructor.anotherMethod (foo ()) () in
Foo.someMethod foo num
A notable example is the ArrayConstructor
type in ES5 (https://github.com/microsoft/TypeScript/blob/main/lib/lib.es5.d.ts).
Simplifies a value definition of an anonymous interface type to a module.
Assume we have the following input:
declare var Foo: {
someMethod(value: number): void;
};
If this option is set, the output will be:
module [@js.scope "Foo"] Foo : sig
val someMethod: float -> unit [@@js.global "someMethod"]
end
(* usage *)
let _ = Foo.someMethod 42.0
Otherwise, the output will be:
module AnonymousInterfaceN : sig
type t = private Ojs.t
val someMethod: t -> float -> unit [@@js.call "someMethod"]
end
val foo: unit -> AnonymousInterfaceN.t [@@js.global "Foo"]
(* usage *)
let _ = AnonymousInterfaceN.someMethod (foo ()) 42.0
A notable example is the Document
variable in DOM (https://github.com/microsoft/TypeScript/blob/main/lib/lib.dom.d.ts).
Note:
immediate-instance
andimmediate-constructor
will override this feature if the name of the value definition is the same as the corresponding interface.
Defines additional module with a suffix Static
for a value definition of some interface type.
Assume we have the following input:
interface Foo = {
someMethod(value: number): void;
}
declare var foo: Foo;
If this option is set, the output will be:
module Foo : sig
type t = ...
val someMethod: t -> float -> unit [@@js.call "someMethod"]
end
module [@js.scope "foo"] FooStatic : sig
val someMethod: float -> unit [@@js.global "someMethod"]
end
val foo: unit -> Foo.t [@@js.global "Foo"]
(* usage *)
let _ = FooStatic.someMethod 42.0
let _ = Foo.someMethod (foo ()) 42.0 (* "instance call" is also available *)
Otherwise, the output will be:
module Foo : sig
type t = ...
val someMethod: t -> float -> unit [@@js.call "someMethod"]
end
val foo: unit -> Foo.t [@@js.global "Foo"]
(* usage *)
let _ = Foo.someMethod (foo ()) 42.0
A notable example is the document
variable in DOM (https://github.com/microsoft/TypeScript/blob/main/lib/lib.dom.d.ts).