Closed jmagaram closed 1 month ago
Ok I think I figured it out. I'm defining a big data model and I want to consume it from TypeScript. The thing I didn't totally understand is that @genType is opt-in, not opt-out, and you can't put the annotation on a first-class module variable or on a module name and expect it to work. I probably don't need first class modules.
First, I need to put @genType on nearly EVERY item of every module. If I consistently use module types and put the annotations there it works. There are a few gotchas...
@genType
and the item it referenced appeared on two separate lines. This made a mess of my code. But if you purposely join the lines the formatter will keep them on the same line. This was a huge discovery.@genType module CompanyName = ...
doesn't seem to do anything and there are no warnings/errors. I know I've gotten confused by this in the past. A warning or something would help point people to correct opt-in usage.export...
syntax..gen.tsx
files, like when I put the annotation on the first class module, are pretty useless. A warning or something explaining opt-in would help. It would be nice (not high priority) if there was an opt-out version where you could put a @genType over a module and it would publish everything that is available like on the ReScript side. If it is available/public to ReScript it is available to TypeScript. I see why having both opt-in and opt-out could be confusing, especially with things that tweak how it works like @genType.as
.
@jmagaram see fix to the docs in https://github.com/rescript-association/rescript-lang.org/pull/655
Feel free to provide PRs to improve other parts of the docs based on your findings.
No still can't reliably get this to work. I really don't know where to begin explaining what is going on. I don't understand the rules and they aren't documented anywhere.
In the example below, companyName1
becomes unknown
but companyName2
is ok. I would assume they should generate identical or similar results. Why are there different results when a module type is inlined?
For some reason I can't use the type elimination operator :=
when I put the type inline. Why not?
If I don't put @genType on either first class module, nothing gets generated. But if I put a @genType let x = 0
somewhere in the file it generates tons of stuff. Likewise if I put that @genType
annotation on top of the CompanyName
module it generates tons of stuff even though I don't think that is supported; you can't annotate a module.
module type T = {
@genType type t
type domain
@genType let make: domain => result<t, string>
@genType let value: t => domain
}
module Make = (
C: {
type domain
let cmp: (domain, domain) => float
let validate: domain => result<domain, string>
},
): (T with type domain = C.domain) => {
type t = C.domain
type domain = C.domain
let value = i => i
let make = (i: domain) => i->C.validate
}
module CompanyName = Make({
type domain = string
let cmp = Js.String2.localeCompare
let validate = i => {
let len = Js.String2.length(i)
len > 1 && len < 20 ? Ok(i) : Error("Wrong length")
}
})
// Can not use domain := string in place; must define separate module type. Why?
module type CompanyNameType = T with type domain := string
@genType
let companyName1: module(CompanyNameType) = module(CompanyName)
@genType
let companyName2: module(T with type domain = string) = module(CompanyName)
Here is a different situation where I assigned @genType properly in my modules but it isn't working. The gen.tsx
code references a type t
that isn't defined anywhere. I think in this situation it will not work if you use include in module types where the module is generated. But if you use the expanded (non-include) module type at the end - where you designate the first-class-module - it won't work; the damage has been done. Is it a known limitation to never use include
in module types that have @genType annotations?
Maybe these two examples are the same. In the example above, I did module type CompanyNameType = ...
and tried to use that in the first-class module definition and it didn't work. Maybe any time you assign a module type to another module type, or do some kind of transformation like include
, all the @genType annotations are lost?
module type T = {
@genType type t
type domain
@genType let make: domain => result<t, string>
@genType let value: t => domain
}
module Make = (
C: {
type domain
let validate: domain => result<domain, string>
},
): (T with type domain = C.domain) => {
type t = C.domain
type domain = C.domain
let value = i => i
let make = i => i->C.validate
}
module type UniqueId = {
// Works
// @genType type t
// @genType let make: string => result<t, string>
// @genType let value: t => string
// @genType let getRandom: unit => t
// Does NOT work
include T with type domain := string
@genType let getRandom: unit => t
}
@module("nanoid")
external customAlphabet: (string, int) => (. unit) => string = "customAlphabet"
module MakeUniqueId = (
C: {
let minLength: int
},
): UniqueId => {
include Make({
type domain = string
let validate = i => Js.String2.length(i) < C.minLength ? Error("too short") : Ok(i)
})
let builder = customAlphabet("ABCDEFG012345", C.minLength)
let getRandom = () => builder(.)->make->Result.getExn
}
module LongUniqueId: UniqueId = MakeUniqueId({
let minLength = 20
})
@genType
let longUniqueId1: module(UniqueId) = module(LongUniqueId)
I'm not sure if I need first-class modules or not for exporting. Again I just ran an experiment where I'm creating a module module UserId = MakeString(...
and no .gen.tsx
is made. But if I add a @genType let genTypeTrigger = true
it seems to generate everything I need like UserId_make
etc.
What you're observing is basically a combination of
Because of this, this entire area is full of unexpected surprises. If you're interested, it would be useful to clean up some of this. At least document some of these gotchas, or perhaps put up big disclaimers in the docs. Some of the limitations one might get around by just changing some little detail that was overlooked as this is not a widely used combination. Others might be solvable, but at a huge complexity cost which is not justifiable (deeply change the type checker). Others won't be solvable, as the concepts just don't map to TS.
So I guess starting from a few best practices and documenting what's out there would come first. Followed by seeing what are more compelling use cases which are currently not supported and could be supported in future.
Some inconsistency about using include
in module types. This first type does NOT work, although it is the most concise and what I really want. The .gen.tsx
references a type t
that isn't defined.
module type NanoId = {
include Primitive.T with ...
@genType let makeRandom: unit => t
...but this does work - all the CmpUtilities too - even though it also has an include
module type NanoId = {
(most of the stuff from Primitive.T explicitly listed)
include CmpUtilities.T...
@genType let makeRandom: unit => t
I'd be happy to write some documentation. I'm not capable of doing any dev work on this. But I can't yet figure out what rules to follow.
I think of ReScript modules as a perfect analogue to ES6 modules; I don't see the disconnect with TypeScript. A ReScript module is just a collection of exported functions and types, just like an ES6 module. It seems to work well IF I don't use functors and include
. I don't think my need here is about first class modules. It's about modules. If I've built a NanoId
module in ReScript and I'm using it I can see its got 6 different functions and 1 opaque type. I just want to publish it so I can use it from TypeScript, as if I had marked everything inside with @genType
. I don't care if the module was built with functors and include etc. I care about the end result type - what I see in the intellisense in VS Code.
It sounds like what you're saying is that by the time it gets to the intellisense in VS Code all the @genType annotations are lost. I tried applying @genType annotations after-the-fact, doing...
@genType let nanoId : module(type that I want with lots of genType annotations) = module(NanoId)
...but it didn't work. It was too late.
I'm surprised what I'm trying to do is so unusual. I'm trying to model the data domain of my app. Functors help with that. I've got a Validated
functor to make specific types for the different kinds of data in my domain and they handle serialization, validation, and other type-specific needs. I want to consume this data model from TypeScript because I'm trying to do my UI in SolidJS. Even with React, there are also some parts of my app where I don't want to have to write bindings, and so it is easier to consume the data in TypeScript.
I might post a question on the forum to see what people are using genType for and if anyone has guidance for me. I'm really surprised more people aren't having the difficulties I am.
I'm not sure if I need first-class modules or not for exporting. Again I just ran an experiment where I'm creating a module
module UserId = MakeString(...
and no.gen.tsx
is made. But if I add a@genType let genTypeTrigger = true
it seems to generate everything I need likeUserId_make
etc.
This sounds like a simple inconsistency that should be fixed. Would you provided a minimal self-contained example?
I'm not sure if I need first-class modules or not for exporting. Again I just ran an experiment where I'm creating a module
module UserId = MakeString(...
and no.gen.tsx
is made. But if I add a@genType let genTypeTrigger = true
it seems to generate everything I need likeUserId_make
etc.This sounds like a simple inconsistency that should be fixed. Would you provided a minimal self-contained example?
Might be fixed in master already: see the changelog here https://github.com/rescript-lang/rescript-compiler/pull/5903/files#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4edR48
Adding it to the upcoming 10.1.3 too https://github.com/rescript-lang/rescript-compiler/pull/6026
Funny I was just going to ask you about this as a real bug. I just did a include NanoId.Make(...
in a .res
file and nothing got generated. Did the @genType let trigger = ...
and everything I needed was built.
This is not likely to receive much attention, compared to other aspects of the compiler, given the lack of compelling use cases seen so far.
The general advice is to keep the code simple instead. As complex constructs do not come for free in terms of implementation cost.
Ok. Close bug if you want.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
According to the docs at genType first class modules I should be able to use syntax like
export ...
but this doesn't work. So maybe the docs need to change.I have been struggling with how to share my modules with TypeScript. Originally I was sprinkling @genType annotations all over the place but I couldn't figure it out. I tried decorating modules and members of modules. I tried putting the @genType annotation on the interfaces and on the implementations. Sometimes I used @genType.ignoreInterface. Just couldn't get reliable results. Perhaps part of the problem is I'm using functors. Ideally I could slap a @genType on the final module that was built with a bunch of functors and
includes
.And so today I thought maybe I could have a single file called
TypeScriptInterop.res
and I'd put a bunch of first class modules in there, slap @genType on them, and it would all work. And since I have to manually specify the type of these modules, it would seem like genType has all the information it needs regardless of whether they were built with functors and include statements. But no. Here is a single file. I'm trying to makeCompanyName
available to TypeScript.And here is the
.gen.tsx
...