Rust for a TypeScript Developer
I came to Rust after years of TypeScript. TypeScript gives JavaScript projects useful static checks while keeping the runtime and ecosystem familiar. Rust asks for more precision, especially around ownership and lifetimes, and returns tighter control over memory and execution.

The first weeks were slower than working in TypeScript. The compiler rejected programs that looked reasonable to me, and many errors concerned values I had stopped thinking about after handing memory management to a garbage collector. Following those errors was the useful part of learning the language.
Ownership makes lifetime visible
Rust enforces ownership rules at compile time. A value has an owner, moving it transfers that ownership, and borrowing gives temporary access under rules checked by the compiler.
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
}This program does not compile because assigning s1 to s2 moves the String. The final line tries to use a value that no longer owns its allocation.
The rule felt restrictive at first. It also made resource lifetime visible in ordinary code. The compiler catches use-after-free, double-free, and many invalid cross-thread accesses before the program runs.
TypeScript developers usually rely on garbage collection and rarely choose when an object is released. That is the right tradeoff for many applications. Rust becomes interesting when predictable memory use, low latency, or direct control over resources matters.
Native concurrency
JavaScript commonly coordinates asynchronous work on an event loop, with workers available when code needs parallel execution. Rust supports native threads directly and checks how data crosses thread boundaries.
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}The example starts one thread, continues on the main thread, and waits for the spawned work before exiting. More interesting programs share data, which is where Rust's Send and Sync traits and ownership rules prevent a large class of races.
Abstractions without runtime bookkeeping
Rust monomorphizes many generic functions at compile time. A generic helper can retain a high-level interface without introducing dynamic dispatch or boxing unless the program asks for them.
fn add<T: std::ops::Add<Output = T>>(a: T, b: T) -> T {
a + b
}
fn main() {
let a = 2;
let b = 3;
let c = add(a, b);
println!("{}", c);
}The compiler generates code for the concrete type used at the call site. The abstraction helps the source code without requiring a generic runtime representation.
What carried back to TypeScript
Rust made me more deliberate about ownership even in garbage-collected programs. I pay closer attention to who may mutate data, how long a resource should live, and whether concurrency is explicit in an interface. Those questions improve TypeScript services too.
I would choose TypeScript for many web products because iteration speed, browser support, and its package ecosystem are valuable. Rust is a better fit when the runtime budget is tight, native integration matters, or a service benefits from stronger control over memory and concurrency.
Learning Rust did not replace TypeScript in my work. It gave me another model for reading systems code and a better vocabulary for tradeoffs that managed runtimes usually hide.
Building something with AI?
I help small businesses turn ideas into software that pays off. Tell me what you’re working on and get a free first assessment.