Generate TypeScript Bindings for Rust
Rust is a really great language when it comes to developing back-end web applications, it offers type safety, and extreme performance. This makes Rust a desirable choice to develop back-end web applications.
However, all applications need JavaScript or TypeScript on the client or the front-end, TypeScript adds type safety for JavaScript, makes it easier to refactor and catch errors early in the development process. However, if your back-end is written in Rust, you'll need a way to communicate between the front-end and the back-end.
One way to do that is to write TypeScript interfaces in your front-end manually based on how the Rust back-end sends back the response.
However, this is not very efficient, you'll have to spend a lot of time making sure the types from the backend and front-end match exactly, there will be a big room for error, and will be time consuming to make sure all types match exactly to the backend responses.
In this blog post, we'll use a Rust library crate ts-bind, this crate will help us generate TypeScript bindings automatically based on our Rust code so we don't need to manually write code and sync them, and all of our types can be in the same place.
Using ts-bind
ts-bind provides a derive macro TsBind that can be used on top of Rust structs and it will automatically find the corresponding TypeScript type that matches the JSON output of the Rust struct.
Install the crate:
How to use ts-bind
Using the library is pretty easy, you just need to add the TsBind
derive macro on top of your Rust struct and it will automatically generate the TypeScript type for you.
Let's create a simple struct and use the TsBind
derive macro:
Now, we don't need to do anything at all, the TsBind
derive macro will automatically generate the TypeScript type for us in a directory named bindings/
in the root of our project. If you don't see it make sure you save the file and run cargo build
.
The generated TypeScript file will look like this:
More complex types
The TsBind
derive macro can also handle more complex types, like nested structs and more complex types. Let's update the struct above and add a few more fields:
The generated TypeScript file will look like this:
ts-bind
will automatically generate the TypeScript types for the nested struct as well and it will import the nested struct in the main struct ensuring complete type-safety.
Optional fields and Arrays
TypeScript types can be optional, and can also be arrays, ts-bind
can handle that as well, let's update the User
struct to include an optional field and an array:
The generated TypeScript file will look like this:
Features
ts-bind
provides a few features you can tweak to modify the output TypeScript code to your liking, you can find the features documentation to read more about it, but we'll go over a few of them here.
Renaming the generated TypeScript file
The TsBind
macro has an attribute called ts_bind()
which we can use on the struct itself and even the fields to do modifications on them, let's say we have a struct called BlogPost
and we want to rename the generated TypeScript file to PostType
:
The generated TypeScript file will be in bindings/BlogType.ts
:
Renaming to a specific casing
It's fairly common in TypeScript and JavaScript to use camelCase instead of the snake_case we use here in Rust, so it would be a good idea to have an output of camelCase while sticking to our Rust convention with Rust's snake_case.
We can use the ts_bind(rename_all = "..")
attribute to do that.
Generated TypeScript
You can also provide PascalCase
, snake_case
, UPPERCASE
, lowercase
if needed to modify the fields to the corresponding casing.
Custom export directory
You can also have your own custom directory by providing the ts_bind(export = "...")
attribute and provide a custom directory.
Generated TypeScript code
Renaming fields
You can also rename individual fields, by using the ts_bind(rename = "...")
attribute on the specific fields.
Generated TypeScript code
Skipping fields
Sometimes you want to omit a field in the output TypeScript file, for that you can use the ts_bind(skip)
attribute on the field you want to omit.
Generated TypeScript file:
Building an Axum backend
Now that we've had an overview on how to generate TypeScript bindings for our Rust code, let's simulate a real-life example by creating an Axum backend and generating TypeScript bindings for the routes that we create.
Axum is a Rust backend framework that is built on top of Tokio, it's very fast and efficient and has a lot of useful features.
Let's create a new Rust application named rust-typescript
:
Adding dependencies
We need a few dependencies to make our Axum backend work:
axum
- The main Axum librarytokio
- The async runtime that we'll need to run our Axum applicationserde
- The serialization and deserialization library for Rustts-bind
- The library we'll use to generate TypeScript bindings
Run the following commands to add the dependencies:
Here's how your Cargo.toml
should look like, your version numbers might be different:
Let's create a simple Axum application and then build on top of it:
We'll create a simple route that returns a struct of User
.
If we go to http://localhost:3000
we should see the JSON response of the User
struct.
Axum response
Great, now the API works, let's generate TypeScript bindings for the User
struct.
Generating TypeScript bindings
We'll use the ts-bind
crate to generate TypeScript bindings for the User
struct, we'll add the TsBind
derive macro on top of the User
struct and then we'll see the generated TypeScript file.
This will automatically create you a typescript file in the bindings/
directory with the name User.ts
:
Using the TypeScript bindings
Even though this blog post isn't necessarily about how to use TypeScript, I'll show you how you can use the generated TypeScript bindings in your front-end.
The best way to do it is having a TypeScript Monorepo where you can have your front-end and back-end in the same repository, you can then make your Rust backend another package by adding a package.json
to it and then you can import the TypeScript bindings in your front-end, this goes beyond the scope of this blog post, but we'll assume that you already know that and have set up your TypeScript Monorepo and installed your Rust backend as a package.
After you added your Rust backend as a package in your TypeScript Monorepo, you can import the TypeScript bindings in your front-end like this:
This will give you type safety in your front-end, and you can be sure that the response from the backend is exactly what you expect it to be.
Front-end response
If we try to access a field that doesn't exist in the User
struct, TypeScript will throw an error:
TypeScript error
Property 'address' does not exist on type 'User'
, this is the type-safety that we want. Now if we were to add an address
field in the backend response, the changes will instantly be reflected in the TypeScript project as well.
TypeScript error fix
The error goes away instantly, giving you a seamless developer experience.
Conclusion
Using ts-bind
to generate TypeScript bindings for Rust code offers several key benefits, you can automatically synchronize your TypeScript types with your Rust structs, ensuring type consistency between your backend and frontend, catching potential issues early in the development process, and making it easier to keep your codebase in sync.
This also gives your an enhanced developer experience, with proper type definitions, developers don't have to go back and forth to look at the responses that the backend sends, they can just look at the TypeScript bindings and see what the response is.
Type-safety also helps in easier refactoring, it's common for projects to need refactoring especially if they grow quickly, with this kind of automatic type synchronization, refactoring becomes easier and less error-prone.
Thank you for reading this blog post 🤗, we hope you found it useful. If you enjoyed this one, you might also enjoy our Rust exercises, give it a go and see how you do!