It’s very common for developers to switch from one programming language to another. For instance, you might convert a JavaScript application to TypeScript to take advantage of the latter’s code maintainability and reusability benefits.
Did you know that you can also do this with Rust? Rust and TypeScript each bring their own advantages and drawbacks, and learning to write apps in both Rust and TypeScript will help you become a better, more versatile developer.
In this tutorial, we’ll show you how to transition from writing apps in Rust to TypeScript and vice versa. We’ll explore how these languages are similar, how they differ, and common challenges associated with each. We’ll then discuss the benefits of adopting TypeScript in Rust as well as best practices for how to do so.
Here’s what we’ll cover:
Any
in TypeScript and Unsafe
in RustRust manages its dependencies in cargo.toml
file. Once you create your Rust project, this file is created in the root of your project. To import a new dependency into your project, add it to your cargo.toml
file.
// cargo.toml file [package] name = "example@helloworld" version = "0.0.1" authors = ["Helloworld"] [dependencies] chrono = "0.4" futures = "0.3" atk = "^0"
After adding the dependency in your cargo.toml
file, run your project.
The way TypeScript manages dependencies is quite similar to how Rust manages dependencies. All you need to do is create a TypeScript project, then a package.json
file will be created in the root of your project folder.
// package.json file { "name": "example@helloworld", "description": "", "version": "1.0.0", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "https://github.com/me/[email protected]" }, "keywords": [], "author": "Helloworld", "license": "ISC", "bugs": { "url": "https://github.com/me/example@helloworld/issues" }, "homepage": "https://github.com/me/example@helloworld" }
Next, install your dependencies. You can use a package manager such as npm. The dependency you installed will appear in your package.json
file.
Unlike Rust, you don’t need to import dependencies into your project by editing your package.json
file. When you install the dependency, it registers in package.json
. If you add the dependency in package.json
without installing it (like in Rust), you’ll be prompted to install it while you try to run your project.
Rust and TypeScript are both statically typed languages. This means the compiler tries to infer the data type during compilation.
Let’s look at the Rust example below:
fn main() { // NUMBERS let i:i32 = 1; println!("The value of i is: {}", i); // 1 // This will throw an error at compile time because 1.1 is type f64 let j:i32 = 1.1; // error[E0308]: mismatched types, expected `i32`, found floating-point number println!("The value of j is: {}", j); // 1.1 }
While TypeScript is statically typed, it doesn’t actually enforce that you’re accessing the right data type at compile time.
const i:Number = 1; console.log(`The value of i is: ${i}`); // 1 const j:String = 1.1; console.log(`The value of j is: ${j}`); // 1.1
The program above throws an error at compile time: expected `i32`, found floating-point number
. However, the same code is executed in runtime. This act can cause more problems and may result in an exception.
To avoid that, the program below uses a TypeScript library runtypes. It not only gives a warning at compile time, it enforces data type checking whenever you’re accessing a different type from what you assigned.
// install library npm install --save runtypes import { Boolean, Number, String, Literal, Tuple, Record, Union } from 'runtypes'; const Vector = Tuple(Number, Number, Number); const AcademicStaff = Record({ type: Literal('academicStaff'), location: Vector, population: Number, }); const Student = Record({ type: Literal('student'), location: Vector, population: Number, punctual: Boolean }); const NonAcademicStaff = Record({ type: Literal('nonAcademicStaff'), location: Vector, population: Number, name: String, }); const UniversityObject = Union(AcademicStaff, Student, NonAcademicStaff); // spaceObject: SpaceObject const universityObject = UniversityObject.check(obj);//check will throw an exception if the object doesn't conform to the type specification
Rust, as a statically typed programming language, allows exceptions to be caught in time. For data types, the compiler can infer the data type of the value you assigned.
This method helps to capture errors at compile time, before runtime. That way, you don’t use exceptions for flow control, which impacts the performance of your code. You can know confidently that you program won’t throw an error at runtime once it compiles successfully.
The way Rust handles errors ensures there are no exceptions. For errors that are recoverable, there’s a Result<T, E>
type. When errors are not recoverable, a panic!
macro stops the execution of the program at compile time.
// This error isn't recoverable fn main() { let v = vec![1, 2, 3]; v[99]; } // This is a rust recoverable error use std::fs::File; fn main() { let f = File::open("hello.txt"); } // A typical rust program that utilizes panic! to get results from a recoverable error (file not found) use std::fs::File; fn main() { let f = File::open("hello.txt"); let f = match f { Ok(file) => file, Err(error) => panic!("Problem opening the file: {:?}", error), }; }
Here’s an example of how TypeScript handles exceptions with a try
, catch
statement.
// Typescript runs program, shuts at exceptions try { try_statements } [catch [(exception_var)] { //what if you're not really catching the exception? catch_statements }] [finally { finally_statements }]
The problem with the try
, catch
method is that it handles runtime errors exclusively. Therefore, the compiler can’t tell you when there’s an error in your try
, catch
block. Furthermore, you can’t use try
, catch
in asynchronous codes because it will finish executing before the asynchronous code finishes its execution. TypeScript’s type-safe error handling with type Type<T>
handles exceptions more capably.
<// How to handle exceptions in Typescript codes better, type Result<T> = T | Error; export type Type<T> = Result<T>; export function isError<T>(result: Result<T>): result is Error { return result instanceof Error; } export function isSuccess<T>(result: Result<T>): result is T { return !isError(result); } function doIt(): Result.Type<Thing>{ if(true) { return {name: "Todd"}; } else { return new Error("boom") } }
Writing conditional statements in TypeScript can quickly become ambiguous. To solve this problem, most developers use the switch case
statement in TypeScript. However, this also becomes verbose over time.
type Option<A> = { _tag: 'Some'; value: A } | { _tag: 'None' } type Foo = { _tag: 'Foo' } type Bar = { _tag: 'Bar' } type FooBar = Foo | Bar declare const foobar: FooBar switch (foobar._tag) { case 'Some': // Double switch! switch (foobar.value._tag) { case 'Foo': break case 'Bar': break } break default: break }
The example above shows how verbose TypeScript can become when you’re writing conditional statements in nested ADTs. That’s because each level of nesting requires a switch statement.
The same example can be rewritten in Rust, as shown below:
pub enum FooBar { Foo, Bar } fn main() { let foobar: Option<FooBar> = Some (FooBar::Foo); match foobar { Some(FooBar::Foo) => {} Some(FooBar::Bar) => {} _ => {} } }
Any
in Typescript and Unsafe
In RustTypeScript’s Any
type and Rust’s unsafe
share a lot of similarities. They are mostly used for unlocking additional features. For instance, Rust’s unsafe
enables you to dereference a raw pointer, access an unsafe function, and so on.
fn main() { unsafe fn dangerous() { println!("I'm Unsafe") //prints I'm Unsafe } unsafe { dangerous(); } }
With the any
type in TypeScript, you can access properties without restrictions, even ones that don’t exist. For example, developers can access functions and TypeScript won’t check their type:
let Hello: any = 4; // OK, ifItExists might exist at runtime, it dosen't exist now. Hello.ifItExists(); // OK, itExists exists (but the compiler doesn't check) Hello.itExists();
From the Rust example, we just assessed an unsafe function. With this program, Rust doesn’t enforce memory safety and allocates on the heap. Variables from TypeScript’s any
type are also stored in the heap since the variable type can change in the course of execution.
A mutable object or variable can be changed during the course of execution even after assigning a value. In Rust, variables are immutable by default because the compiler wants to make sure you don’t assign values to a variable more than once.
You don’t have to keep track of where a value might change. When you run the program below, the compiler throws an error:
fn main() { let a = 5; println!("The value of a is: {}", a); a = 6; println!("The value of a is: {}", a); }
Typescript is not designed for variable immutability. However, if you use the keyword const
to assign values to variables, you can’t assign them to another value:
const a = 5; console.log(a); a = 6; console.log(a);
The program above will throw an error at runtime when it gets to a = 6;
. That’s because it realizes that a
is a constant and another value shouldn’t be assigned to it.
There are times when mutability is beneficial or even necessary. You can mutate variables in Rust by adding mut
in front of the variable. Let’s look at the first example we treated:
fn main() { let mut a = 5; println!("The value of a is: {}", a); a = 6; println!("The value of a is: {}", a); }
The program above runs without error because we added mut
to the variable. In TypeScript, you can mutate variables of type let
and var
, as shown in the example below:
//let let a = 5; console.log(a); a = 6; console.log(a); //var var a = 5; console.log(a); a = 6; console.log(a);
Rust has the const
keyword. However, you can only set the variable value at compile time alone, not at runtime.
fn another_function(x: i32) -> i32 { return x + 1; } fn main() { // RUN-TIME ASSIGNMENT, if you replace const with let, there's no compile error const z = another_function(5); println!("The value of z is: {}", z); // 6 }
Because let
can be set at compile time and const
can’t, the code throws an error at compile time. Although Rust variables are immutable by default, you can redefine or shadow variables of type let
:
fn main() { let a = 5; let a = a + 1; let a = a * 2; println!("The value of a is: {}", a); // 12 }
In TypeScript, you can only shadow or redefine variables of type var
by default:
var a = 6; var a = a + 1; var a = a * 2; console.log(a);
Objects are a very important part of TypeScript. You can think of an object as a person that has different characteristics, called properties. You want to be able to link and access those characteristics of the person as if they’re linked together:
var person = { name: 'Ugochi', country: 'Nigeria', age: 10 } console.log(person.name); //Ugochi console.log(person.country); //Nigeria console.log(person.age); //10
However, in Rust, you don’t create objects themselves. You create an instance with a struct
type. Structures are called structs
in Rust. Structs put related properties into one data type.
Let’s recreate the object sample from our TypeScript object with a struct:
fn main() { let person = Person { name: String::from("Ugochi"), country: String::from("Nigeria"), age: i32::from(10) }; println!("{}", person.name); //Ugochi println!("{}", person.country); //Nigeria println!("{}", person.age); //10 } struct Person { name: String, country: String, age: i32, }
You can build objects from other objects in TypeScript, as shown in the example below:
const Manager = { name: "John", age: 27, job: "Software Engineer" } const intern = Manager; console.log(intern.name); //John console.log(intern.age); // 27 console.log(intern.job); // Software Engineer
Structs, too, can be built from other structs:
fn main() { let person = Person { name: String::from("Ugochi"), country: String::from("Nigeria"), age: i32::from(10) }; println!("{}", person.name); //Ugochi println!("{}", person.country); //Nigeria println!("{}", person.age); //10 let intern = person; println!("{}", intern.name); //Ugochi println!("{}", intern.country); //Nigeria println!("{}", intern.age); //10 } struct Person { name: String, country: String, age: i32, }
One reason why developers love Rust is its memory management capabilities. In Rust, you don’t have to worry about how the garbage collector cleans up the memory that’s no longer required from the heap. All you need to do is request for memory from the memory allocator. Rust automatically returns this memory to the allocator when you’re done.
fn main() { let mut s = String::from("Welcome"); s.push_str(", Ugochi!"); // push_str() appends a literal to a String println!("{}", s); // This will print `Welcome, Ugochi!` }// This scope is over and s is no longer valid.
In the example above, we requested memory from the memory allocator. Since we don’t know the size of the string (it can be changed in the program mut
), it’s sent to the heap instead of the stack.
Immediately, the scope is over. Rust returns the memory s
was occupying back to the heap. We don’t need to write programs to clean up or return memory back to the heap when we are done with them.
TypeScript uses a garbage collector, which manages memory from the heap. Just like in Rust, it does this automatically.
Let’s look at the example below from Mozilla:
var y = { a: { b: 2 } }; // two objects are created. One is referenced by being assigned to the 'y' variable // The other is referenced by the other as one of its properties. // None can be garbage collecte. var y = x; // The 'x' variable is the second thing that has a reference to one of the object. y = 1; // The object that was originally in 'y' has a unique reference from the 'x' variable. var z = x.a; // Reference to 'a' property of the object. // The object now has 2 references: one as a property, the other as the 'z' variable. x = 'mozilla'; // The object that was originally in 'y' has no zero references to it. It can be garbage-collected. // However its 'a' property is still referenced by the 'z' variable, so it cannot be freed. z = null; // The 'a' property of the object originally in 'y' has no references to it. It can be garbage-collected.
The memory is not released back to the heap at the end of the scope because a property of the variable is still in use.
Portability and stability are two common reasons to switch from one programming language to another. What about a language that supports multiple operating systems and architecture?
Rust allows cross-compiling. It has binary builds of the compilers for Windows, MacOS, Linux etc. You can use binary builds of any OS to build for other architecture.
Although TypeScript doesn’t offer as much portability as Rust, it can be run on virtually any OS you can think of. You don’t need to install any runtime environment to execute TypeScript code.
The most common reason for switching to Rust is its guaranteed memory safety. Developers who choose TypeScript usually value its intuitiveness and type safety in JavaScript.
Switching your application from one programming language to another can be stressful and complicated. You may wonder whether it’s better to just leave the application stack the way it is. No one wants to be a latecomer in their field; as the frontend and web development ecosystems evolve, it’s critical to find your place in it. Learning to write the same types of apps and perform the same functions in multiple programming languages can help you become a more well-rounded developer.
In this article, we demonstrated how to switch from TypeScript to Rust and vice versa. We also walked through some challenges you’re likely to encounter when making this change and explored the many similarities between TypeScript and Rust.
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Modernize how you debug your Rust apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
3 Replies to "Switching from Rust to TypeScript (and vice versa)"
“You don’t need to install any runtime environment to execute TypeScript code.” That’s not really true… you first need a TypeScript to Javascript, compiler, and then you need a Javacript interpreter runtime (embedded in a browser or in nodejs) to run your program. Granted, for many people they already have a runtime installed on their system, but for some applications this could be a deal-breaker.
Good read for primer. Thanks
“Rust has the const keyword. However, you can only set the variable value at runtime alone, not at compile time.
fn another_function(x: i32) -> i32 {
return x + 1;
}
fn main() {
// RUN-TIME ASSIGNMENT, if you replace const with let, there’s no compile error
const z = another_function(5);
println!(“The value of z is: {}”, z); // 6
}
Because let can be set at compile time and const can’t, the code throws an error at compile time. Although Rust variables are immutable by default, you can redefine or shadow variables of type let:
”
I’m pretty sure you meant run-time and compile-time the other way around for this section (the typo is in the text, not in the code).
Thanks for reading and for pointing out that typo. It should be fixed now.