Learning Rust as a TypeScript Developer: The Honest Version
I want to write the version of this post that is not a highlight reel. Most learning Rust as a JS developer posts I read before starting followed the same arc, initial confusion, borrow checker fights, then a triumphant breakthrough where everything suddenly makes sense. My experience was messier than that, and six months in I still occasionally lose an afternoon to a lifetime error that a TypeScript developer's brain simply has no framework for anticipating.
Why I even started
Our team needed to write a CLI tool that processed large log files, gigabytes at a time, and our existing Node-based tooling was choking on memory usage in a way that a streaming rewrite in JS could probably have fixed but that felt like the wrong long-term investment given where the tool was headed. I picked Rust specifically, not Go, partly out of curiosity and partly because a coworker who had made the same jump a year earlier promised the type system would feel like a natural extension of strict TypeScript rather than a totally foreign paradigm. That promise was about sixty percent true.
What genuinely felt familiar
The type system itself was a smaller leap than I expected. If you have used TypeScript's discriminated unions seriously, Rust's enums with associated data are not a conceptual stretch at all, just a stricter, more deeply integrated version of the same idea.
enum ParseResult {
Success { value: String, line: usize },
Failure { reason: String, line: usize },
}
fn handle(result: ParseResult) {
match result {
ParseResult::Success { value, line } => println!("line {}: {}", line, value),
ParseResult::Failure { reason, line } => eprintln!("line {}: {}", line, reason),
}
}
This is genuinely just a switch on a discriminated union with exhaustiveness checking, something any TypeScript developer using strict mode already reaches for constantly. The match expression forcing exhaustiveness at compile time, rather than TypeScript's exhaustiveness check which you have to opt into with a never assertion in the default case, actually felt like a small upgrade rather than a new concept. Option and Result types mapped cleanly onto patterns I already used, wrapping nullable values and error-prone operations explicitly instead of throwing exceptions, which is a discipline plenty of good TypeScript codebases already enforce through convention even without the compiler forcing it.
What genuinely did not feel familiar
Ownership and borrowing is the part every tutorial warns you about, and the warnings are correctly calibrated, it is a real conceptual gap, not just unfamiliar syntax. The core idea, that a value has exactly one owner and the compiler tracks when references to it are valid, has no real analog in a garbage-collected language, because in TypeScript you simply never think about who owns a value, the runtime handles that question invisibly for you at all times.
The first genuinely frustrating wall I hit was trying to write something as simple as a function that returns a reference into a struct it also mutates.
struct LogBuffer {
lines: Vec
}
impl LogBuffer {
fn last_line(&self) -> &String {
self.lines.last().unwrap()
}
fn add_and_report(&mut self, line: String) -> &String {
self.lines.push(line);
self.last_line()
}
}
This specific shape, mutably borrowing self to push, then immutably borrowing it again to read, would not compile as I originally structured it, because I had accidentally tried to hold onto the mutable borrow from push while also creating an immutable borrow through last_line in the same scope, something the borrow checker rejects even though the actual runtime sequence of operations is perfectly safe. It took me embarrassingly long, and a genuinely helpful compiler error message pointing at the exact conflicting borrow, to understand that the fix was just reordering so the mutable borrow's lifetime ends before the immutable one begins. What eventually made this click was accepting that the borrow checker is not modeling my code's runtime behavior, it is modeling a conservative, provably safe subset of possible behaviors, and sometimes code that is obviously safe to a human still needs restructuring to be provably safe to the compiler's more limited, rule-based analysis.
The moment it actually clicked
It was not one moment, honestly, it was somewhere around the third or fourth time I hit a borrow checker error, correctly guessed the fix before reading the full error message, and the guess was right. That is a low bar, and I want to be honest about how unglamorous the actual learning curve felt from the inside, mostly it was just repetition building intuition, not some conceptual epiphany. Reading the official Rust book's ownership chapter twice, a year apart, months apart, helped more than any single blog post or video, because the second read landed against actual scar tissue from real compiler errors instead of abstract examples.
Was it worth it
For the specific problem, yes, unambiguously. The log processing tool went from choking on files above a couple gigabytes in our old Node implementation to comfortably streaming through files an order of magnitude larger, with peak memory usage that stayed flat regardless of file size because we were processing line by line rather than loading everything into memory. That is a genuine, measurable win that would have taken real, careful engineering effort to achieve in JavaScript even with streams, and Rust's ownership model made a memory-safe streaming implementation almost the path of least resistance rather than something I had to fight for.
For my general skill as an engineer, I think the bigger value was not Rust itself but what fighting the borrow checker taught me about implicit assumptions I had been carrying around in every language, including TypeScript, about shared mutable state. I catch myself now, writing plain JavaScript, noticing patterns where two parts of the code hold references to the same mutable object and mutate it from different places, and thinking about that the way I would think about a borrow conflict, even though JavaScript will happily let me do it without complaint. That habit of mind, more than any specific Rust syntax, is the thing I did not expect to carry back into my day job, and it has quietly made me a little more careful in every language I touch since.
If you are a TypeScript developer considering this same jump, my honest advice is to pick a real, bounded project with a genuine reason to exist, not a toy exercise, because the frustration of fighting the borrow checker is much easier to push through when there is a real payoff waiting on the other side rather than an abstract sense that you should be learning Rust. I would also say, gently, that six months is not actually that long in the context of learning a genuinely different paradigm, and I still catch myself reaching for patterns that feel more natural in a garbage-collected language before remembering there is often a more idiomatic Rust way to express the same idea. That is fine. Fluency in a second paradigm, the same way fluency in a second spoken language works, comes in layers over a much longer timeline than the first few breakthroughs make you expect, and I have stopped being impatient with myself about the layers that have not clicked yet.
Related Posts
Sponsor Our Newsletter
Reach thousands of developers who are actively evaluating AI tools, MCP servers, and dev infrastructure. Our weekly newsletter goes to engaged technical decision-makers.
All sponsored content is clearly labeled per our editorial policy.