Rust vs TypeScript

When to Use Which?

Use Rust for high-performance, memory-safe systems programming and TypeScript for rapid, scalable web development with a vast ecosystem.

The runtime reality

You are building a command-line tool that processes gigabytes of log files. You start in TypeScript because you know the syntax and the ecosystem. It works perfectly for a 100MB file. You try a 10GB file. The process consumes all available RAM. The garbage collector pauses for seconds at a time, freezing the terminal. The CPU fans scream. The tool takes twenty minutes to finish.

You switch to Rust. The memory usage stays flat at a few megabytes. The CPU usage drops. The tool finishes in three minutes. This isn't about Rust being "better" in a vacuum. It's about the runtime constraints of your problem. TypeScript runs on a virtual machine with a garbage collector. Rust compiles to native machine code with no runtime overhead.

Concept in plain words

TypeScript and Rust solve different problems. TypeScript adds static types to JavaScript. It helps you catch mistakes before the code runs, but the runtime is still JavaScript. JavaScript runs on engines like V8 or SpiderMonkey. These engines use a garbage collector to manage memory. The garbage collector automatically reclaims memory when objects are no longer used. This makes development fast and prevents memory leaks. It also introduces non-deterministic pauses. The collector stops your program to clean up. The frequency and duration of these pauses depend on heap size and allocation patterns.

Rust compiles directly to machine code. It runs on the CPU without a virtual machine. It manages memory manually but safely using the borrow checker. There is no garbage collector. Memory is freed the moment a value goes out of scope. This gives you predictable performance and low memory usage. It also requires you to follow strict rules about how data is shared and mutated. The compiler enforces these rules at compile time. If your code violates them, it doesn't compile.

Think of TypeScript like a well-organized office with a cleaning crew. The crew comes in periodically, throws away trash, and reorganizes. While they work, you can't use the desk. Rust is like a workshop where you clean as you go. You never stop working, but you have to follow strict rules about where tools go and who holds them. If you break the rules, the foreman stops you before you start.

Minimal example

Here is a simple data structure in both languages. The syntax looks similar, but the semantics differ.

/// Represents a user with a name and age.
struct User {
    name: String,
    age: u32,
}

/// Greets the user and returns a formatted string.
fn greet(user: &User) -> String {
    // Borrow the user to read fields without taking ownership.
    // The & symbol indicates a reference.
    format!("Hello, {}! You are {} years old.", user.name, user.age)
}

fn main() {
    let user = User {
        name: String::from("Alice"),
        age: 16,
    };

    // Call greet with a reference. The user value stays valid.
    let message = greet(&user);
    println!("{}", message);
}

In TypeScript, you would define a class or interface. Passing an object to a function passes a reference implicitly. In Rust, you must be explicit. The &User type tells the compiler you are borrowing the value. You are not taking ownership. The user variable in main remains valid after the call. If you passed user without the &, you would move the value into the function, and it would be invalid in main. The compiler rejects this with E0382 (use of moved value).

Explicit references prevent accidental data loss. Trust the borrow checker.

Walkthrough

When you run the Rust code, the User struct is allocated on the heap because String contains heap data. The user variable on the stack holds a pointer to that heap allocation. When you call greet(&user), the compiler creates a reference on the stack pointing to the same heap data. The borrow checker verifies that the reference is valid for the duration of the function call. It ensures no mutable references exist while the immutable reference is active.

Inside greet, you read the fields. No allocation happens for the reference itself. The format! macro allocates a new String for the result. When greet returns, the reference is dropped. The heap data remains alive because user in main still owns it. When main ends, user goes out of scope. The String is dropped, and the heap memory is freed immediately. No garbage collector is involved. The cleanup is deterministic and zero-cost.

In TypeScript, the User object lives on the heap. The garbage collector tracks references to it. When you pass the object to a function, the reference count increases. When the function returns, the reference count decreases. The collector periodically scans the heap, finds objects with zero references, and reclaims them. This happens asynchronously. You cannot predict when the memory will be freed. Under high allocation pressure, the collector runs more often, causing pauses.

Realistic example

Error handling highlights another major difference. TypeScript uses exceptions. You wrap risky code in try/catch blocks. Errors propagate up the call stack automatically. This is convenient but can hide bugs. An error thrown deep in a dependency might be caught by a generic handler that logs nothing useful.

Rust uses the Result type. Functions return either a success value or an error. You must handle both cases explicitly. This forces you to think about failure modes.

/// Tries to parse a number from a string.
/// Returns the number or a descriptive error.
fn parse_config_value(input: &str) -> Result<i32, String> {
    // Attempt to parse. If it fails, return an error immediately.
    // map_err transforms the parse error into a custom message.
    input.trim().parse::<i32>().map_err(|e| format!("Invalid number: {}", e))
}

fn main() {
    // Handle the Result explicitly. No silent failures.
    match parse_config_value(" 42 ") {
        Ok(val) => println!("Config value: {}", val),
        Err(e) => eprintln!("Error: {}", e),
    }
}

Rust developers love the ? operator for propagating errors. It keeps the happy path readable. You can rewrite main to use ? if the function returns a Result. The ? operator returns the error immediately if the Result is Err, otherwise it unwraps the Ok value. This convention reduces boilerplate while preserving explicitness.

Convention aside: Always prefer &str over String for function parameters when you only need to read the data. &str is a slice that borrows data. It avoids allocation and works with string literals, String values, and substrings. Accepting String forces the caller to allocate, which is rarely what you want.

Explicit error handling prevents silent failures. Handle the error where you have context to recover.

Pitfalls and friction

Switching from TypeScript to Rust introduces friction. The borrow checker fights you when you try to share mutable state. You try to mutate a collection while iterating over it. The compiler rejects you with E0502 (cannot borrow as mutable because it is also borrowed as immutable). In TypeScript, this works fine but might cause subtle bugs if you modify the array length during iteration. Rust forces you to separate reading and writing. You collect indices first, then apply mutations. Or you use RefCell for interior mutability if the borrow checker is too strict for your data flow.

Allocation patterns also differ. TypeScript developers allocate objects freely. The garbage collector handles the rest. Rust developers must be mindful of allocations. Every String, Vec, or Box allocates on the heap. Excessive allocation slows down your program and increases memory usage. You learn to use stack-allocated types, slices, and references to avoid heap pressure. The Cow type helps when you might or might not need to allocate. It stands for "Clone on Write." It borrows data by default and clones only when mutation is required.

Async programming is another gap. TypeScript has async/await built into the language and runtime. Rust has async/await in the language, but futures are lazy. A future does nothing until you poll it. You need an executor like tokio or async-std to drive futures to completion. This adds complexity. You must choose an executor and understand its constraints. Rust async is more powerful but has a steeper learning curve.

Don't fight the borrow checker. Refactor the data flow to match the rules.

Decision matrix

Use TypeScript when you are building a web frontend and need to interact with the DOM. Use TypeScript when you are prototyping a backend API and value development speed over raw performance. Use TypeScript when you need to leverage the massive ecosystem of npm packages for web standards. Use TypeScript when your team is already proficient in JavaScript and you want to minimize onboarding time.

Use Rust when you are writing a command-line tool that processes large files and needs predictable memory usage. Use Rust when you are building a library that other languages will call via FFI. Use Rust when you are developing embedded software or drivers where a garbage collector is unacceptable. Use Rust when you need fine-grained control over concurrency without data races. Use Rust when you are optimizing a performance-critical path and have measured that the current implementation is the bottleneck.

Rust isn't a replacement for TypeScript. It's a different layer of the stack.

Interoperability and ecosystem

You don't always have to choose one or the other. Rust and TypeScript can work together. You can write performance-critical code in Rust and compile it to WebAssembly. The WebAssembly module runs in the browser alongside your TypeScript code. You call Rust functions from TypeScript with near-native speed. This is common for image processing, cryptography, and physics simulations in web apps.

On the backend, you can use Rust for heavy computation and TypeScript for the API layer. You might run a Rust microservice that handles data transformation and call it from a TypeScript Express server. Or you can use neon or napi-rs to write Node.js addons in Rust. These tools let you expose Rust functions to Node.js with minimal overhead.

The ecosystem maturity differs. npm has hundreds of thousands of packages. Crates.io has fewer packages but higher quality standards. Rust packages are generally well-tested and maintained. The cargo toolchain is batteries-included. It handles building, testing, formatting, and publishing. cargo fmt formats every file the same way. clippy lints your code for common mistakes. You don't need to configure a dozen tools to get started. TypeScript tooling is fragmented. You choose between tsc, swc, esbuild, and various bundlers. Configuration can be complex.

Convention aside: Run cargo clippy before every commit. It catches subtle bugs and suggests idiomatic improvements. Treat its warnings as errors in CI to maintain code quality.

Pick the tool that matches the constraints of your problem. Combine them when the problem spans multiple domains.

Where to go next