DEV Community

Cover image for Beyond TypeScript πŸš€
Mahmoud Harmouch
Mahmoud Harmouch

Posted on β€’ Edited on β€’ Originally published at opensass.org

5 1 1 2 1

Beyond TypeScript πŸš€

Hey devs, and anyone still dealing with a 900MB node_modules folder,

We're constantly being told that JavaScript and TypeScript are the "standard" for web development. "Everyone uses it", they say. "It's easy", they whisper. But you know what else was popular? Internet Explorer.

Today, we break the matrix. We're not going for what's easy. We're going for what's right. And Rust, my friends, is the real deal. We're about to explore why Rust outshines TypeScript on every level.

Let's cook.

1. Rust Doesn't Need ORMs Because It Is the Data Model

Let's talk databases. In TypeScript, if you want to interact with a database safely and cleanly, you're often forced to use an ORM like Prisma (They dropped Rust for TypeScript., We're not mad, just disappointed.), or TypeORM. These ORMs generate types based on your schema, or vice versa. Sounds good in theory. But in reality, you're juggling SQL migrations, type generation, inconsistent runtime checks, and you're praying to the gods of npx every time you need to change a column name.

Rust? You define a struct, and that's your schema. Your struct is strongly typed, compile-checked, and used directly with your query code. You don't need a third-party tool to tell you what a "User" is. You write the shape once, and it's the source of truth. No scaffolding. No runtime surprises.

Let's explore an example using MongoDB in Rust:

use bson::{oid::ObjectId, serde_helpers::chrono_datetime_as_bson_datetime};
use chrono::prelude::*;
use serde::{Deserialize, Serialize};

#[allow(non_snake_case)]
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct User {
    #[serde(rename = "_id")]
    pub id: ObjectId,
    pub name: String,
    pub email: String,
    pub password: String,
    pub role: Role,
    pub photo: String,
    pub verified: bool,
    #[serde(with = "chrono_datetime_as_bson_datetime", rename = "createdAt")]
    pub created_at: DateTime<Utc>,
    #[serde(with = "chrono_datetime_as_bson_datetime", rename = "updatedAt")]
    pub updated_at: DateTime<Utc>,
}
Enter fullscreen mode Exit fullscreen mode

This isn't just a struct; This is your database document schema, your gatekeeper, and your compiler-approved contract with the data layer. Every field is mapped directly, every date is serialized exactly how Mongo expects it, and Rust ensures you don't mess up a single byte.

Want to insert a user? Here's how clean and safe it looks:

let new_user = User {
    id: ObjectId::new(),
    name: "OpenSASS".into(),
    email: "oss@opensass.org".into(),
    password: "hashed_password".into(),
    role: Role::Admin,
    photo: "tob_tobi_tob_cat.png".into(),
    verified: true,
    created_at: Utc::now(),
    updated_at: Utc::now(),
};

let collection = db.collection::<User>("users");
collection.insert_one(new_user).await?;
Enter fullscreen mode Exit fullscreen mode

You're still using MongoDB - a famously schemaless, ultra-flexible NoSQL database - but with Rust, your schema is no longer optional. It's enforced. It's structured. It's safe. Your compiler will scream before you ever send malformed data to your database.

Meanwhile, in TypeScript? You'll define a Prisma schema, wait for it to regenerate types, run a migration, and then cross your fingers that the app doesn't crash on an optional field because some foreign key constraint failed silently.

2. Rust's Type System Actually Prevents Bugs. TypeScript's Just Says "Good Luck".

Let's stop calling TypeScript "safe." It's NOT. TypeScript is like a guard dog that barks loudly... but is chained up and can't actually stop intruders. You can tell TypeScript, "this should be a string", and then immediately say, "but I'm going to treat it like an any anyway". The compiler lets you do that. The code runs. The bug lives.

Rust, on the other hand, is strict. Rust doesn't let you cheat. It doesn't care if you're late, tired, or really need this feature pushed today. If your types don't match, your code doesn't compile. No exceptions. That's not frustrating; That's freedom. Because once it builds, it works.

Let's explore a TypeScript example, broken, but it compiles:

type User = { name: string };
const getUser = (): any => ({ id: 123 });

const user = getUser() as User;
console.log(user.name); // undefined at runtime, no error until it's too late
Enter fullscreen mode Exit fullscreen mode

Rust, on the other hand, won't even compile:

struct User {
    name: String,
}

fn get_user() -> i32 {
    123
}

fn main() {
    let user: User = get_user(); // error[E0308]: mismatched types
}
Enter fullscreen mode Exit fullscreen mode

That's the beauty. Rust stops you before disaster. TypeScript lets you slide down the hill and then says, "Oopsie doopsie".

And if you want really rich type systems? Rust has traits, generics, enums with data, pattern matching, and more. TypeScript dreams of that kind of power but settles for Partial<T> and Record<string, T> hacks.

3. Rust Does Real Concurrency. TypeScript Just Juggles Tasks.

JavaScript's concurrency model is basically: one thread + hope. It's like a waiter taking 20 orders at once and telling customers, "I'll get to you eventually". It works for small apps and frontend tasks. But for serious backends, real-time apps, or high-performance systems? It's a liability.

Rust offers true multi-threading, shared memory, message-passing channels, and async/await with zero garbage collector overhead. You can create threads, spawn async tasks, and handle millions of requests concurrently without leaking memory or blocking your core thread.

Let's explore a Rust concurrency example using Tokio:

use tokio::task;
use tokio::time::{sleep, Duration};
use futures::future::join_all;

#[tokio::main]
async fn main() {
    let tasks = vec![
        task::spawn(compute_something("Task A")),
        task::spawn(compute_something("Task B")),
    ];

    join_all(tasks).await;
}

async fn compute_something(name: &str) {
    println!("{} is working...", name);
    // Some heavy async work
    sleep(Duration::from_secs(2)).await;
}

// Output

// Task A is working...
// Task B is working...
Enter fullscreen mode Exit fullscreen mode

You get true parallelism when needed. And memory is managed safely, no race conditions, no thread panics unless you explicitly allow it. Compare that to TypeScript's:

async function main() {
  await Promise.all([doSomethingA(), doSomethingB()]);
}
Enter fullscreen mode Exit fullscreen mode

That looks clean, but it all runs on the same thread. Under pressure, it's going to choke. Rust's runtime is optimized for massive loads and deep concurrency, and it won't leave dangling promises in the dark.

4. Rust's Compiler Teaches You. TypeScript Just Complains.

Let's talk compiler errors. In TypeScript, errors are vague. They're short. They make you question your life choices. Rust's compiler, on the other hand, is like a wise elder, it doesn't just reject your code, it tells you why, where, how to fix it, and sometimes even what you probably meant.

Let's take another look at our previous Rust example:

struct User {
    name: String,
}

fn get_user() -> i32 {
    123
}

fn main() {
    let user: User = get_user();
}

// Output

// error[E0308]: mismatched types
//   --> src/main.rs:10:22
//    |
// 10 |     let user: User = get_user(); // error[E0308]: mismatched types
//    |               ----   ^^^^^^^^^^ expected `User`, found `i32`
//    |               |
//    |               expected due to this
//
// For more information about this error, try `rustc --explain E0308`.
// error: could not compile `playground` (bin "playground") due to 1 previous error
Enter fullscreen mode Exit fullscreen mode

Not only does it tell you the line, the variable, and the mismatch. It shows you exactly what to fix. This makes Rust a learning experience. It's hard at first, but over time it levels you up.

In TypeScript, you'll often encounter:

Type 'string' is not assignable to type 'number'.
Enter fullscreen mode Exit fullscreen mode

Cool. Butt... which line? What variable? How do I fix it?

Rust wants you to get better. TypeScript wants you to suffer quietly.

5. Memory Safety in Rust Isn't Optional. In TypeScript, You Can Leak Like a Faucet.

Rust is built around ownership and lifetimes. These are core concepts that guarantee memory is safely handled. When a variable goes out of scope, it's dropped. You don't need a garbage collector. You don't need to delete things manually. And you don't need to worry about slow runtime memory leaks.

TypeScript? You've got a GC. Cool. But GCs are unpredictable. Memory leaks happen when references aren't cleaned up properly, closures hold onto state, event listeners pile up, unused variables chill in RAM, and suddenly your Node.js app is eating 2GB of memory for no reason.

Let's take a look at a Rust example:

fn allocate_data() {
    let data = vec![1, 2, 3];
    // data is freed automatically when it goes out of scope
}
Enter fullscreen mode Exit fullscreen mode

No leak. No GC. No surprises. You write the code, and Rust ensures memory is released safely, every single time.

6. Rust's Tooling Works Out-of-the-Box. TypeScript Devs Live in Dependency Hell.

Let's talk about dev setup. With TypeScript, the typical project setup requires Node, npm/yarn/pnpm, tsconfig.json, eslint, prettier, Jest, ts-node, webpack or Vite, and maybe a dozen other tools just to get a basic dev environment running. That's before you even write a single line of code. Every time you start a new project, you basically have to rebuild the wheel. You Google "best tsconfig", copy-paste some Stack Overflow answer, and cross your fingers. And once you get it running, there's still a weekly stream of broken dependencies and config nightmares.

Rust? It gives you everything you need with one tool: cargo. You install Rust using rustup, and suddenly you have a compiler (rustc), a package manager (cargo), a formatter (rustfmt), a linter (clippy), a test runner, a doc generator, and a build tool. All built-in. No plugins, no config chaos. You type cargo new my_proj, and you're coding. That's it.

Want to format your code? Deal!

cargo fmt
Enter fullscreen mode Exit fullscreen mode

Want to lint it? Gotcha!

cargo clippy
Enter fullscreen mode Exit fullscreen mode

Want to build it? Cool!

cargo build --release
Enter fullscreen mode Exit fullscreen mode

Want to run tests? Awesome!

cargo test
Enter fullscreen mode Exit fullscreen mode

All of this just works, everywhere. No hours lost on setting up a CI pipeline. No guessing what version of TypeScript is compatible with what version of Webpack. In Rust, your productivity is compounding, not constantly being reset.

7. Rust's Error Handling is Real. TypeScript Just Throws and Hopes.

TypeScript error handling is like a seatbelt made of spaghetti. Sure, you can try/catch, but exceptions in JavaScript are a runtime mess. Everything is a possible undefined, so half the code is checking for nulls, typeof === "function", and truthy/falsy values. You throw errors using throw, but catching them is a gamble. If something fails deep in the call stack, and you didn't catch it early, you're cooked.

Rust doesn't do runtime errors like that. It uses Result<T, E>, a built-in type that forces you to handle both success and failure. The compiler enforces it. If you call a function that can fail, you must deal with the error. No unhandled exceptions. No hidden undefineds. Just crystal-clear control flow.

Let's consider a Rust example:

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err("Division by zero".to_string())
    } else {
        Ok(a / b)
    }
}

fn main() {
    match divide(10, 0) {
        Ok(result) => println!("Result is {}", result),
        Err(e) => println!("Error: {}", e),
    }
}

// Output

// Error: Division by zero
Enter fullscreen mode Exit fullscreen mode

This is type-safe, explicit, and predictable. You can use combinators like .map(), .and_then(), or even the elegant ? operator to propagate errors up the stack. That means you get all the power of exceptions, but with no runtime chaos.

8. Rust's Enums Are Algebraic Data Types. TypeScript's Unions Are Just Strings with Hats.

TypeScript has something called union types, like type Shape = 'circle' | 'square' | 'triangle'. It's better than plain JS, sure. But they're flat. You can't associate meaningful data with each variant cleanly. You have to use additional structures or type guards. And if you forget one case in a switch, nothing warns you.

type Shape = "circle" | "square" | "triangle";

function calculateArea(shape: Shape, size: number): number {
  switch (shape) {
    case "circle":
      return Math.PI * size * size;
    case "square":
      return size * size;
  }
  return 0;
}
Enter fullscreen mode Exit fullscreen mode

Rust's enum is a full-featured algebraic data type. Each variant can carry its own structure, and pattern matching on them is exhaustive. If you forget to handle a variant, the compiler slaps your wrist before you run the code.

Let's consider a Rust example:

enum Shape {
    Circle { radius: f64 },
    Square { side: f64 },
    Triangle { base: f64, height: f64 },
}

fn area(shape: Shape) -> f64 {
    match shape {
        Shape::Circle { radius } => 3.14 * radius * radius,
        Shape::Square { side } => side * side,
    }
}

// Output

//  --> src/lib.rs:8:11
//   |
// 8 |     match shape {
//   |           ^^^^^ pattern `Shape::Triangle { .. }` not covered
//   |
// note: `Shape` defined here
//  --> src/lib.rs:1:6
//   |
// 1 | enum Shape {
//   |      ^^^^^
// ...
// 4 |     Triangle { base: f64, height: f64 },
//   |     -------- not covered
//   = note: the matched value is of type `Shape`
// help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
//   |
// 10~         Shape::Square { side } => side * side,
// 11~         Shape::Triangle { .. } => todo!(),
//   |
Enter fullscreen mode Exit fullscreen mode

This is expressive. It's powerful. It's compiler-enforced safety. TypeScript can't touch this. At best, you get a combination of interfaces, union types, and runtime type guards, and even then, it can still go wrong. In Rust, once it compiles, it's solid.

9. Rust's Traits Enable Powerful, Generic Code. TypeScript Has Duck Typing and Hopes.

In TypeScript, if two objects look the same, they're considered the same; That's duck typing. It's flexible, but also fragile. You can accidentally "satisfy" a type without actually meaning to. There's no real interface enforcement at runtime. And generic constraints? They're shallow.

Rust has traits, which are contracts for behavior. If a type implements a trait, the compiler guarantees that it provides the required methods. You can write generic functions and structs that work across types, as long as those types implement the necessary traits. This gives you real polymorphism, enforced by the compiler, with zero overhead.

Let's consider a Rust example:

trait Printable {
    fn print(&self);
}

struct Document;
struct Image;

impl Printable for Document {
    fn print(&self) {
        println!("Printing document...");
    }
}

impl Printable for Image {
    fn print(&self) {
        println!("Printing image...");
    }
}

fn print_all<T: Printable>(items: Vec<T>) {
    for item in items {
        item.print();
    }
}
Enter fullscreen mode Exit fullscreen mode

This is generic, reusable, and strictly typed. There's no ambiguity. No unexpected duck behavior. Just clean, reliable abstraction. And it works at zero cost, meaning there's no performance penalty for using these abstractions.

10. Rust Has WebAssembly Muscle. TypeScript Just Compiles to More JS.

WebAssembly is the future of high-performance web apps, and Rust is the king of Wasm. You can write entire parts of your frontend in Rust, compile them to Wasm, and get performance on par with native code, running in the browser. Not just that. You also get memory safety, type safety, and way better performance than JS for CPU-heavy tasks.

TypeScript? You compile it to JavaScript. That's all it ever does. It can't go lower. It can't go native. It can't do real-time rendering or heavy lifting without choking.

Let's consider a Rust example:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}
Enter fullscreen mode Exit fullscreen mode

Compile with wasm-pack build, and boom. You have a lightning-fast WebAssembly module, callable from JavaScript.

Rust is being used in production WebAssembly apps, graphics engines, real-time simulations, and more. If you're serious about performance in the browser, Rust isn't optional. It's essential.

11. Rust Has a Strong, Growing Ecosystem. TypeScript Is Still Tied to JavaScript's Chaos.

Let's be honest: a huge part of TypeScript's ecosystem is just wrappers around JavaScript libraries. It's bolted-on typing. Most TS libraries are ports or bindings of JS ones, and you still deal with the old JS quirks, any types, and version mismatches.

Rust? It's built clean from the ground up. The crates.io registry is full of modern, safe, composable libraries. You've got Axum, Rocket and Actix for backends, Leptos, Dioxus, and Yew for frontend, and more. Every library you use follows the same philosophy: safety, performance, and zero tolerance for ambiguity.

You can write a frontend, backend, CLI, and even a game engine, all in one language. You don't need separate runtimes, tools, or platforms. Rust does it all, and it does it better.

Final Word, for now...

If you've made it this far, you're either a true believer or you're furiously writing a rebuttal in VS Code with three broken npm packages. Either way, the truth is clear:

Rust is better than TypeScript. Not just "better". It's a different league. A language for real software.

We're not saying TypeScript is bad. We're saying Rust solves problems TypeScript can't even define. If you want to build reliable, performant, error-free systems, Rust is your new home.

We are Open SASS, babe!.

We're working tirelessly on making Rust web development extremely easy for everyone.

If you made it this far, it would be nice if you could join us on Discord.

Together, let's move the web beyond JavaScript, and into something that actually compiles.

Feel free to share this. Fork it. Turn it into a meme. Tattoo it on your CI pipeline. Tell your manager Rust is your spirit animal now.

Top comments (6)

Collapse
 
leob profile image
leob β€’ β€’ Edited

Cool article !

I will readily believe that Rust is superior to Typescript for large/complex apps, or for speed/performance critical tasks - however, it is it also worth it for your average, fairly simple, run-of-the-mill app - does it provide substantial benefits over TS in those more "basic" scenarios? (arguably 95% of the apps being developed ...)

Also - is it possible to code a React (or Vue, or Angular, or ...) app completely in Rust?

I did some quick googling, and my impression is the answer to that is "no" - you can develop pieces of the app in Rust, e.g. functions that encapsulate business logic, and then call those from your React app via WASM - but coding a React app completely in Rust (not using any JS/TS) is currently a no-go ... link to a relevant article:

dhiwise.com/post/the-evolution-of-...

Anyway, my point is that even if Rust is great as an extra tool in your toolkit, I wouldn't ditch JS/TS yet - "rumors of JS/TS-es demise are greatly exaggerated" ;-)

Collapse
 
wiseai profile image
Mahmoud Harmouch β€’

Thanks for the reply!

Rust is a highly capable language that can be used to build applications of any size or complexity. The idea that Rust isn't suitable for prototyping or small projects is an outdated misconception. With modern frameworks like Dioxus, it's now easier than ever to build fully cross-platform applications in 100% Rust, no JavaScript required.

At Open SASS, I am actively proving that Rust can be used as the sole language for developing production-grade SaaS applications. Every project showcased on the Open SASS homepage is built entirely in Rust, frontend, backend, static site generation and blog. Not a single line of JavaScript is involved.

This isn't theory; It's happening in practice. Rust is not only viable for all layers of app dev; it's also a powerful alternative that eliminates the need for polyglot stacks. I'm fully committed to making Rust accessible to React developers by demonstrating how seamless it can be to transition to a 100% Rust workflow.

If you are coming from React, frameworks like Yew provide a familiar component-based paradigm, making the migration process much easier. Of course, I acknowledge that compiling Rust can sometimes be a pain point. But that’s a solvable issue. and one worth enduring for the benefits Rust offers in performance, safety, and long-term maintainability.

Rust is ready. The ecosystem is evolving fast, and we're here to lead that charge.

Collapse
 
leob profile image
leob β€’

Thanks, amazing ... keep up the good work!

Thread Thread
 
wiseai profile image
Mahmoud Harmouch β€’

Thanks! On it!

dababy is working over time

Collapse
 
nevodavid profile image
Nevo David β€’

dang ive spent way too long fighting with typescript builds so this hits hard - you think folks stick with js because its easier to learn, or just because the tools are everywhere?