Skip to content
Patrick Desjardins Blog
Patrick Desjardins picture from a conference

My experience migrating TypeScript libraries to Rust

Posted on: 2023-02-16

I recently migrated a couple of Rust libraries from TypeScript using a subset of features of well-known TypeScript libraries. So far, I have the following Rust repositories:

I am still a novice with the Rust language and ecosystem but I enjoy learning by practicing. Here are a few takeaways from these fours projects

Configuration Files

Instead of using a package.json you have a cargo.toml. Very similar, with one difference: you do not have all the scripts that you use. Only the metadata of your application (name, version, author, etc.) and dependencies on other packages.

I'm using the readme.md to document the commands that I often use like generating a build, test, coverage, etc.

Outside the cargo.toml, a small file name rust-toolchain.tom specifies which version of Rust you are using. All my projects have:

[toolchain]
channel = "stable"

I also have a bash file in all my projects for code coverage. It performs the test with some environment variables and cleans up the generated files from grcov. The bash file: ­

CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='coverage-%p-%m.profraw' cargo test
grcov . -s . --binary-path ./target/debug/ -t html --branch --ignore-not-existing --excl-line "///" -o ./target/debug/coverage/
rm coverage*.profraw
rm default*.profraw

At first, I was confused about how to install a library since I rely on npm install <lib here>. In Rust, you can use a command, but writing in the cargo.toml is enough. Then, VsCode will know that the file has changed and get the Intellisence. And the library downloads during the build step.

Main and Lib

One exciting aspect of Rust is that you have a terminal entry point and a library entry point in your project. Hence, when you deploy your application, it is available from a terminal and any other Rust project. It is not required, but I found it very useful. I always have my main.rs referencing my lib.rs.

It means that anyone can download your terminal application by installing your code (which will download the code, build and install it into your $path);

For example:

cargo install ripgrep
rg --help

Mod, Pub Mod, and Use

A confusing aspect is the use of mod. There is a new and deprecated way to manage files and modules with Rust. In short, you should no longer rely on a file named mod.rs anymore.

Mod

Instead, you create a file with the exact same name as the folder with all your Rust (.rs) files.

src
├── lib.rs
├── main.rs
├── utils
│   ├── binary.rs
│   ├── encryption.rs
│   ├── function.rs
│   └── options.rs
└── utils.rs

­ A simple way to understand mod is to see the instruction has a way of declaring that you will be accessing a specific module.

Pub Mod

In that example, the utils.rs defines the module (mod utils), which module is available for the lib.rs and main.rs by making it public (pub mod) outside the folder with pub. Coming from TypeScript/JavaScript is similar to having an index.ts that exports a collection of functions, classes, and interfaces.

For example, in the utils.rs:

pub mod function;
pub mod options;
pub mod binary;
pub mod encryption;

Inside your lib.rs you must call mod utils and then optionally use use to create shortcuts.

// Access
mod utils;

// Imports
use crate::utils::function::{add_message_to_image, get_message_from_image};
use crate::utils::options::SteganographyOption;

A way to visualize the pub mod is to see if the instruction has it copies all the content of the module inside the invoked. In that case, we would copy all the public traits, functions, etc., of the module function, options, binary and encryption into the util mod. Instead, we could erase these four files and have all the code directly into the utils.rs but that would not be an organized code.

Use

So, use is always optional. In that case, because we are defining two functions and one trait, we can now directly call:

add_message_to_image(.....)
// or 
get_message_from_image(....)
// or
SteganographyOption

Without the use we would need to :

crate::utils::function::add_message_to_image(.....)
// or 
crate::utils::function::get_message_from_image(....)
// or
crate::utils::options::SteganographyOption

A simple way to understand use is to create an alias to avoid repetitive typing words.

Tests

Testing has some benefits and inconveniences. First, you do not need to struggle to configure anything. It is available right away. Rust patterns of having unit tests directly in the file are interesting, but also you can have project test and even test in your comments. Code in comment opens the door to an inconvenience: the tooling needs to be more suboptimal for unit tests with comments. No color or auto-complete.

Here is an example of code that is executed if you execute cargo test

Debugging works until it does not. So far with Visual Studio Code, testing worked well, but I had few evenings where it stopped working without much explanation. Is the issue VsCode, Rust, lldb or because I'm working in wsl? Too many layers of potential failure. However, debugging a NodeJS in TypeScript can be painful. I have a project with half a dozen launch configurations depending on the year it was working with a specific testing framework. Rust relies on lldb, so you need to install something besides Rust. It would be great if it were slightly more in-the-box. However, no mapping issue or breakpoint that cannot be hit once everything is working.

Running unit tests is slow in Rust. Also, I have not found a native way to run failing tests (watch mode), making the process even slower. Code coverage code is more integrated as well.

On the negative side, my tests are more organized in TypeScript or other languages because, in Rust, you cannot nest your test, leaving them flat per file. For example, in TypeScript (with Jest) you can have many levels of describe, which help with organizing per categorie.

Mocking a structure is impossible if you want to mock only a portion. Moreover, coming from other languages, it does not seem intuitiv.

Documentation

Using Cargo third-party is a fantastic experience. With all third-party libraries, you can from VsCode see the source code and the documentation.