TypeScript for Rust Developers — A Totally Not Confusing Guide
Coming from the world of Rust, I found myself thrown into a large TypeScript project last year. The transition was... interesting. I spent the first month fighting with the type system and missing my trusty borrow checker. It felt like moving from a strict German boarding school to a California art commune, suddenly no one was checking if I cleaned my room, but everyone wanted to talk about their feelings.

But as I spent more time with TypeScript, I started to appreciate how it brings type safety to JavaScript while maintaining flexibility. It's like JavaScript finally decided to grow up and get a real job, but still keeps its concert t shirts in the closet.
For Rust guys venturing into the world of web development, TypeScript offers a familiar yet different approach. Having now worked extensively with both languages, I want to share the mental models and patterns that helped me bridge these two worlds.
Type Systems: Similar Goals, Different Approaches
Both Rust and TypeScript have powerful type systems, but they serve slightly different purposes. Rust's type system is designed to ensure memory safety without a garbage collector, while TypeScript's goal is to add static typing to JavaScript's dynamic nature.
It's like Rust is your friend who plans their vacation six months in advance with spreadsheets and backup plans, while TypeScript is the friend who has a general idea of where they're going but is "keeping options open."
Let's look at how some Rust concepts translate to TypeScript:
Structs and Interfaces
In Rust, you'd define a struct like this:
ruststruct Developer { caffeine_level: u64, bugs_fixed_today: u32, is_questioning_career_choices: bool, }
In TypeScript, you'd use an interface or type alias:
typescriptinterface Developer { caffeineLevel: number; bugsFixedToday: number; isQuestioningCareerChoices: boolean; } // Or using a type alias type Developer = { caffeineLevel: number; bugsFixedToday: number; isQuestioningCareerChoices: boolean; };
The key difference is that TypeScript's interfaces are open and can be extended, while Rust's structs are sealed:
typescriptinterface User { id: number; name: string; } // Later in another file... interface User { active: boolean; // Extends the original interface }
This openness can be both powerful and problematic, depending on your perspective. It's like the difference between a recipe book (Rust) and a cooking show where the chef says "just add whatever feels right" (TypeScript).
Enums and Variants
Rust's enums are powerful algebraic data types:
rustenum Result<T, E> { Ok(T), Err(E), }
TypeScript has enum syntax, but it's more limited. For Rust-like enums, you'll use discriminated unions:
typescripttype Result<T, E> = | { kind: 'ok', value: T } | { kind: 'err', error: E };
Then you'd use pattern matching (or type guards) to handle different cases:
typescriptfunction handleResult(result: Result<string, Error>): string { switch (result.kind) { case 'ok': return result.value; // TypeScript knows result.value exists case 'err': throw result.error; // TypeScript knows result.error exists } }
Using discriminated unions in TypeScript is like building a DIY furniture kit where you have to manually check if you have all the right pieces before assembly. In Rust, the compiler is like having a professional woodworker constantly looking over your shoulder.
Option and Nullability
Rust's Option<T>
is critical for representing the absence of a value:
rustfn find_motivation(monday_morning: bool) -> Option<Motivation> { if monday_morning { None // Let's be honest here } else { Some(Motivation::new(coffee_cups: 3)) } } match find_motivation(true) { Some(motivation) => println!("Ready to code: {}", motivation.level), None => println!("Please check back after coffee"), }
TypeScript handles this with union types including null
or undefined
:
typescriptfunction findMotivation(mondayMorning: boolean): Motivation | null { if (mondayMorning) { return null; // Brutal honesty } else { return new Motivation({ coffeeCups: 3 }); } } const motivation = findMotivation(true); if (motivation !== null) { console.log(`Ready to code: ${motivation.level}`); } else { console.log("Please check back after coffee"); }
TypeScript's optional chaining operator is like having a friend who always checks if the coffee shop is open before dragging you across town. Very considerate!
typescriptconsole.log(`Found: ${user?.name}`); // Safe access even if user is null
Ownership, Borrowing, and Garbage Collection
The biggest difference between Rust and TypeScript is memory management. Rust's ownership system ensures memory safety without garbage collection, while TypeScript (running on JavaScript) uses garbage collection.
Coming from Rust to TypeScript is like moving from a home where you meticulously sort your recycling into 17 different bins to an apartment with a single chute labeled "We'll Sort It Out!" Sure, it's easier, but you can't help wondering where it all goes.
This means in TypeScript, you don't need to worry about:
- Lifetime annotations (goodbye, cryptic error messages!)
- The borrow checker (no more arguing with an invisible compiler friend)
- Move semantics (everything can be everywhere, all at once)
But it also means you lose compile-time guarantees about resource management. The main thing to understand is that TypeScript objects are always passed by reference, but the reference itself is passed by value:
typescriptfunction modifyUser(user: User) { user.name = "New Name"; // Modifies the original object } const alice: User = { id: 1, name: "Alice", active: true }; modifyUser(alice); console.log(alice.name); // "New Name"
If you want to avoid modifying the original object (similar to Rust's immutability by default), you need to manually create copies:
typescriptfunction modifyUserImmutably(user: User): User { return { ...user, name: "New Name" }; // Creates a shallow copy } const bob: User = { id: 2, name: "Bob", active: true }; const newBob = modifyUserImmutably(bob); console.log(bob.name); // Still "Bob" console.log(newBob.name); // "New Name"
Functional Patterns and Iterators
Both Rust and TypeScript have good support for functional programming patterns. If you enjoy Rust's iterator methods, you'll feel at home with TypeScript's array methods:
Rust:
rustlet numbers = vec![1, 2, 3, 4, 5]; let sum: i32 = numbers.iter() .filter(|&n| n % 2 == 0) .map(|&n| n * n) .sum();
TypeScript:
typescriptconst numbers = [1, 2, 3, 4, 5]; const sum = numbers .filter(n => n % 2 === 0) .map(n => n * n) .reduce((a, b) => a + b, 0);
The main difference is that JavaScript arrays are mutable, so methods like push
and sort
modify the original array. It's like lending a book to a friend who returns it with dog-eared pages and coffee stains. If you want immutability, you need to create copies or use libraries like Immutable.js.
Error Handling Approaches
Rust's Result<T, E>
type and ?
operator make error handling explicit and ergonomic:
rustfn process_file(path: &str) -> Result<String, io::Error> { let mut file = File::open(path)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; Ok(contents) }
TypeScript traditionally uses exceptions, but more functional approaches are gaining popularity:
typescript// Traditional approach with exceptions function processFile(path: string): string { try { const file = fs.openSync(path, 'r'); const contents = fs.readFileSync(file, { encoding: 'utf8' }); return contents; } catch (error) { throw error; // Or handle it } } // Functional approach with a Result type (using a library like neverthrow) function processFile(path: string): Result<string, Error> { return tryCatch( () => fs.openSync(path, 'r'), (error) => error as Error ).andThen(file => tryCatch( () => fs.readFileSync(file, { encoding: 'utf8' }), (error) => error as Error ) ); }
Switching from Rust's Result
type to TypeScript's exceptions feels like going from carefully labeled danger signs to randomly placed banana peels. Sure, you might still avoid the problem, but you won't see it coming!
Async/Await: Similar but Different
Both languages have async/await
syntax, but they work quite differently under the hood:
Rust's async model is based on Futures that don't start executing until they're polled by an executor:
rustasync fn fetch_data() -> Result<String, Error> { let response = client.get("https://api.example.com").send().await?; let body = response.text().await?; Ok(body) }
TypeScript's async functions return Promises that start executing immediately:
typescriptasync function fetchData(): Promise<string> { const response = await fetch("https://api.example.com"); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const body = await response.text(); return body; }
The main difference in practice is that TypeScript's Promises are always "hot" (immediately running), while Rust's Futures are "cold" until awaited in the context of an executor. It's like the difference between a microwave that starts as soon as you punch in a time versus one that waits until you press start.
Tips for a Smoother Transition
After many months of bouncing between Rust and TypeScript, here are my tips for making the transition smoother:
-
Embrace strict TypeScript: Use
--strict
and related flags to make TypeScript more rigorous. This catches more issues at compile time, similar to Rust. It's like putting training wheels on your bike – slightly annoying at first, but prevents a lot of painful face-plants. -
Use immutability where possible: Consider using
readonly
modifiers and avoiding mutation to make your code more predictable. Think of it as labeling your leftovers in a shared fridge. -
Set up good linting: Tools like ESLint with the TypeScript plugin can enforce patterns that make your code more robust. It's like having a spell-checker that also tells you when your writing sounds ridiculous.
-
Try functional libraries: Libraries like fp-ts, io-ts, and neverthrow bring functional programming patterns to TypeScript, including more explicit error handling. It's like packing your favorite comfort food when visiting a foreign country.
-
Don't fight the host environment: TypeScript runs in JavaScript environments (browser, Node.js), which have their own idioms. Sometimes the "JavaScript way" is better than trying to force Rust patterns. It's like trying to drive on the left side of the road when visiting England – fighting local customs just leads to accidents.
TypeScript in the React Ecosystem → Bringing Rust-like Safety to UI
After working with both Rust and TypeScript, I've found React + TypeScript to be a surprisingly comfortable space for Rust developers. Consider this React component:
typescriptinterface CoffeeBreakProps { developer: { id: number; name: string; caffeineLevel: number; lastBreak: Date; }; onCoffeeConsumed: (developerId: number) => void; } const CoffeeBreak: React.FC<CoffeeBreakProps> = ({ developer, onCoffeeConsumed }) => { // Developers below critical caffeine levels need attention! const needsCoffeeUrgently = developer.caffeineLevel < 20; return ( <div className="coffee-break"> <h2>{developer.name}'s Caffeine Status</h2> <p>Current level: {developer.caffeineLevel}%</p> <p>Last break: {developer.lastBreak.toLocaleTimeString()}</p> <button onClick={() => onCoffeeConsumed(developer.id)} disabled={developer.caffeineLevel > 80} className={needsCoffeeUrgently ? "urgent-coffee" : "normal-coffee"} > {needsCoffeeUrgently ? "COFFEE NEEDED IMMEDIATELY!" : "Brew another cup"} </button> </div> ); };
This pattern might seem familiar to Rust developers. Just like Rust's struct definitions and function signatures make data flow explicit, React with TypeScript uses interfaces and type annotations to ensure components receive the correct props. The TypeScript compiler will complain if you try to use a component incorrectly, just as the Rust compiler would prevent you from using a function with incorrect parameters.
It's like Rust's compiler gave you a security detail that follows you everywhere, while TypeScript gives you a slightly more relaxed security guard who mostly checks your ID at the door but occasionally looks the other way if you forgot your badge.
The biggest difference? In Rust, if your code compiles, it probably works. In TypeScript React, if your code compiles, it might still crash at runtime because someone passed null
where they shouldn't have. It's like the difference between having bomb-proof armor (Rust) and wearing a really good raincoat (TypeScript) - one protects you from almost everything, the other just from the most common dangers.
When to Use Each Language
Both languages have their sweet spots:
Rust excels at:
- Performance-critical applications (when milliseconds matter)
- Memory-constrained environments (embedded systems where every byte counts)
- Systems programming (OS kernels, drivers, and other low-level software)
- Applications where runtime safety is paramount (medical devices, financial systems)
TypeScript excels at:
- Web frontend development (its natural habitat)
- Rapid application development (when you need it yesterday)
- Working with the huge JavaScript ecosystem (npm has more packages than stars in the sky)
- Projects where team experience with JavaScript is high (leverage existing knowledge)
TypeScript + React is like the comfortable middle ground for Rust developers dipping their toes in web development. You get much of the type safety you crave without having to deal with lifetime parameters or fighting the borrow checker. It's like switching from chess to checkers – still strategic, just with fewer rules to remember.
I've found the best approach is to use all three: Rust for performance-critical parts compiled to WebAssembly, TypeScript for application logic, and React for the UI layer. It's like using the right tool for the job – you wouldn't hammer in a nail with a screwdriver, but you also wouldn't turn a screw with a hammer (at least, not twice).
Conclusion
If you're a Rust developer learning TypeScript, remember that TypeScript adds types to JavaScript, rather than being a completely different language. Embrace the JavaScript ecosystem and its patterns, while using your knowledge of types from Rust to write more robust code.
Going from Rust to TypeScript is like a classically trained violinist learning to play guitar. The strings are familiar, but the technique is different. You'll hit some wrong notes at first, but your musical foundation will ultimately make you a better guitarist – just don't expect the same precision you're used to.
Adding React to the mix? That's like joining a jazz band after playing in an orchestra. The structure is looser, improvisation is encouraged, but your understanding of music theory (type systems) will help you avoid playing the wrong notes.
Have you made the leap from Rust to TypeScript or vice versa? What patterns and approaches helped you in the transition? Share your experiences in the comments!
Quick Exercise
Take a simple Rust function that uses Result or Option and translate it to TypeScript. Then wrap it in a React component. Compare how error handling feels across the languages. Does TypeScript with React feel like a good compromise between safety and productivity? It's like comparing a safety harness to a safety net – both keep you from hitting the ground, but they feel very different!