Introducing ReScript support in typeshare (Rust)

Hey all,

Typeshare is a nice library for Rust created by 1Password. It allows people to export types defined in Rust to other languages.

I spent a few days creating support for ReScript and it’s now pending merging by the authors. Here’s the tracking issue. If you feel this would be a nice addition to the library, go give it a thumbs up to hopefully make authors notice it. Of course you can also use the fork meanwhile. It should handle the basic cases ok, but you can always ping me if you notice something weird.

So why is this something useful? Say you have a backend API implemented in Rust and want to make sure frontend written in ReScript is always guaranteed to be up to date with the API types. By automatically generating the ReScript types directly from Rust API types it’s possible to ensure this.

The reason I find this specific solution very practical is it allows following nice features:

  • Override certain Rust types with custom ReScript module types. E.g uuid::UuidMyCustomUuid.t
  • Decorators such as @schema for easy rescript-schema/sury support.
  • Handles fairly complex nested enum/struct types and converts them to ReScript variants and records. Or at least tries to, more testing is probably needed.

If you’re using both ReScript and Rust, and have no existing end-to-end typesafety solution, try this out and comment findings in the issue/PR or here.

2 Likes

Ah I had a chat about this recently with @JonoPrest who has probably more thoughts about this than me.
I would love it for desktop apps actually (Tauri).

I hope they’ll merge it.

1 Like

Sidenote: I think you should really not mix the devcontainer config into the rescript commit, maybe even better to remove it or at least to a stacked PR?

They also seem not very frequently active in that repo so might be better to release your own crate nowadays.

Yeah, the devcontainer stuff is just me being lazy. Maybe I should indeed remove it from that PR.

I too noticed there are complaints about inactivity. Let’s see if anything happens. I’d rather not host the whole lib (I barely understood enough of it to make the ReScript part)

1 Like

Nice - excited for this :tada: I actually have a project I was working on yesterday that would benefit from this kind of codegen.

Some edgecases I’ve been thinking about in this case of using rust types as the source of truth and targeting rescript:

  • serde really dictates the wire shape and the type def holds the semantics. It would be nice to be able to mirror this on rescript side. For your uuid example would be nice if it could generate a Sury schema for eg. that describes the string to uuid parsing.
  • best way to handle types from external crates.

Then also would be awesome to have a more multilingual model where the type could be defined in ReScript and target Rust or another language for eg.

2 Likes

Maybe I’ll explain how I use this at the moment to make things a bit more clear. I have this Rust type in backend API code. (yeah, it’s a bit messy with all the macros, but they’re all very necessary)

use uuid::Uuid;

#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[typeshare::typeshare]
pub struct SupplierLinkPayload {
    pub buyer_id: Uuid,
    pub supplier_id: Option<Uuid>,
    pub supplier_name: Option<String>,
}

In the project root I also have a typeshare.toml config file with following content

[rescript]
default_decorators = ["@schema"]

[rescript.type_mappings]
"Uuid" = "Uuid.t" # custom Uuid type
"NaiveDate" = "DateYMD.t" # custom yyyy-mm-dd date type
"DateTime" = "string"     # applies to DateTime<Utc> and DateTime<Local>

When I run command

cargo run --manifest-path ../typeshare/cli/Cargo.toml -- . --lang=rescript --config-file typeshare.toml --output-file=packages/web-client/src/generated/Generated.res

…I get this rescript type in Generated.res

@schema
type supplierLinkPayload = {
	buyerId: Uuid.t,
	supplierId?: Uuid.t,
	supplierName?: string,
}

The custom Uuid.t is implemented in a globally opened Types.res in the project, where it defines it’s own internal type and serialization. So this gives quite a lot of flexibility in client side.

The whole payload type is also directly usable in sury serialization/deserialization and the format is exactly what the Rust API accepts. Of course there is still some work to do with actually calling the API (this is not a client generator in that sense, but personally I don’t even want that because I feel it restricts the client-side implementation too much).

Clearly this whole lib is designed for use cases where Rust is the source of truth for types. (practically speaking Rust is the backend that dictates the client-server boundary). And in that case it’s actually very good that it relies heavily on serde because serde is incredibly powerful (imho). For example, see how camelCase is defined. It’s just a serde macro.

1 Like

Ha, I hoped that that is the case but even better that you confirm it.

Summoning @DZakh for some more potential input.

2 Likes