This content originally appeared on DEV Community and was authored by Ja
Bringing Rust's Result
type to Typescript and giving it a bit of zhuzh
Result
type to Typescript and giving it a bit of zhuzhAdmittedly, I created this implementation of the Result type before diving into constructing a new enum type in TypeScript. Some of it was for fun, but I’m also exploring implementation options for my current project. That said, there’s already a new version of this type, building on concepts I discussed in my previous article on Rust Enums in TypeScript. I'll write a follow-up article once that's complete.
It's 9:00 AM, and you've just opened your laptop. After scrolling past emails about pizza parties and the latest “AI revolution,” you realize your task today is to handle errors—without turning your code into spaghetti.
You’ve been here before. The last time, you tried to ignore error handling, thinking, "Surely nothing will go wrong." Then, like clockwork, you’re wading through a minefield of undefined
values and awkward try/catch
blocks that feel like bubble wrap for your code: a whole lot of work for little payoff.
And here’s the kicker: today’s function is async. Great! Now everything’s a Promise
! Suddenly, your function stack is more tangled than your headphones at the bottom of your bag, and you're juggling await
s like they’re going out of style. But there’s hope: you remember something from the distant land of Rust—a land where Result
types keep error handling sane.
So, the journey begins. What if you could bring a little of that Result
magic to TypeScript? What if, instead of tap dancing around exceptions, you could handle success and failure with the grace of a well-oiled IDE?
What’s a Result
Type? (And Can It Fix My Life?)
Let’s be real: if JavaScript is the Wild West, then TypeScript is the sheriff, walking around town enforcing law and order with types. But sometimes, even TypeScript’s badge isn't shiny enough to save us from errors lurking in the shadows of undefined
.
Rust, being the responsible adult of programming languages, uses a Result
type to make error handling explicit. It’s simple: every function either returns Ok(value)
for success or NotOk(error)
for failure. Nothing gets swept under the rug.
In TypeScript, however, the best we usually get is null
, undefined
, or (heaven forbid) a surprise throw
statement. It's like getting ghosted by your code—one minute it's there, the next... poof.
But what if TypeScript had a Result
type? You could:
- Clearly indicate whether something succeeded or failed, without any surprise parties.
- Chain together a series of operations that may or may not fail, while maintaining the flow of your program.
Which brings us to our hero: the TypeScript Result
type, a Rust-inspired solution to all your error-handling woes.
Function Coloring: Why Your Code Looks Like a Rainbow Gone Wrong
You’ve probably run into the problem of function coloring without even realizing it. It’s that frustrating moment when you write a perfectly simple, synchronous function, but then you throw in a promise... and suddenly everything downstream needs to be async
. It’s like adding garlic to a recipe—now everything tastes like garlic.
Function coloring happens when you mix sync and async operations in a way that makes your code look like a Jackson Pollock painting (except, you know, with await
splattered everywhere). It’s ugly. And debugging it? It’s like playing chess with a blindfold on.
But our friendly Result
type solves this issue by wrapping both sync and async values with the same API. So, no matter what you throw into it, you can handle it in a unified way—whether it’s a plain old number or a promise that resolves after your lunch break.
Introducing the Result
Type: Now in TypeScript Flavor
You might be thinking, "This all sounds great, but how does it work in TypeScript?" Don’t worry, we’ve got you covered. Here's a quick rundown of how the Result
type operates in our universe:
-
Ok(value): The success case. Everything is fine, no alarms, no surprises. Just like finding out your
npm install
actually worked the first time. - NotOk(error): Something went wrong. Your program is still running, but it’s like that one friend who shows up to the party and immediately spills a drink.
- Pending(promise): A result that hasn't resolved yet. This one’s still brewing, like the coffee that’s been "pending" in the office break room for the last hour.
let success = Result.Ok(42);
let failure = Result.NotOk(new Error("404: Code Not Found"));
let pending = Result.Pending(fetch('/api/some-data'));
You’ll notice a beautiful thing here: whether it’s a number, an error, or a promise, you handle everything in the same way. Gone are the days of try/catch
nests and .then()
pyramids.
Composability: The Real MVP
It's 2:00 PM, and you're in the thick of it. You’ve got this complex function with multiple steps: get some data, transform it, maybe do some async processing, and spit out a final result. You’re about to face a nightmare of chained .then()
s, but you remember—you’ve got Result
.
Here’s the beauty of Result
: you can compose functions like a LEGO set, snapping them together with confidence that each piece will either fit or fail gracefully.
let process = compose([
(x: number) => x * 2, // Multiply the input by 2
(x: number) => `${x}px`, // Add some CSS flavor
async (x: string) => x.split("").reverse().join(""), // Flip it around, async-style
(x: string) => Result.Ok({ result: x }) // Wrap it up in an Ok, like a cozy blanket
]);
let result = process(21);
Each function step is passed through the Result
wrapper. Even if one step blows up (like that weird async
thing you wrote at 3 AM), the whole thing doesn’t collapse. The Result
just passes the failure down the line, neatly contained.
Now, let’s imagine this in a normal day:
- You call the composed function.
- Each function gets the output of the last, wrapped safely in a
Result
. - If everything succeeds, you win! If something fails, you get a polite "NotOk" that lets you handle it later, instead of dealing with a screaming
throw
.
And if part of your function needs to wait for some async process? No big deal. Result
will wait patiently like a well-behaved promise. No extra await
s. No .then()
chains that look like modern art. Just clean, readable, color-free code.
Sync vs. Async: The Great Unification
The best part? You no longer have to deal with TypeScript’s split personality between sync and async functions.
With Result
, whether your function returns a regular value or a promise, it’s all wrapped in the same Result
type. So, no more juggling. Just one clean API to rule them all.
const result = Result.Ok(42)
.map(x => x + 1)
.map(x => `${x}px`)
.map(async (x) => await someAsyncFunc(x)); // Whoa, async? Still works!
console.log(await result.getOr("default value"));
There it is—the Holy Grail of modern TypeScript development: sync and async playing nicely together in one tidy chain of Result
s.
Conclusion: Saving Your Sanity, One Result at a Time
So, what have we learned today? By adopting a Result
type in your TypeScript codebase, you can:
- Say goodbye to function coloring problems, and handle async and sync code in a unified way.
- Create beautiful, readable code that actually handles errors without clogging your codebase with endless
try/catch
blocks. - Chain operations together, safely passing values from one step to the next, even if something blows up.
Now, your code can thrive in a world where every function politely returns a result, even when things don’t go as planned.
And you? Well, you can finally close that ticket on error handling without sweating the small stuff—because Result
has your back. Now go ahead, grab another cup of coffee, and take a break. You've earned it.
One Last Thing...
I'm aware that there are dozens of other fairly good ADT / Effects library.... like Effect for instance, which has much of the functionality that I like. I just think its far too bloated for my use case, and is far to verbose to setup your types. I'd like for the type system to assist me forward rather than the other way around.
Also, I may publish this officially on my Github. I was nearly done all my tests, until I fell upon a requirement that I just simply could not ignore, so i've kinda gone back to the drawing board. However, if you would like to play with it, or fork it for yourself, I posted a Gist with the code on Github.
NOTE: this implementation requires TS >= 5.0
PS : This article probably requires some edits, but I'm tired... I've been up all night, and i'm going to bed now... NIGHT NIGHT! I'll expand on a few things later perhaps when I'm better fueled.
This content originally appeared on DEV Community and was authored by Ja
Ja | Sciencx (2024-10-16T17:30:35+00:00) Sync vs. Async: The Odd Couple of Code, Can the `Result` Keep the Peace?. Retrieved from https://www.scien.cx/2024/10/16/sync-vs-async-the-odd-couple-of-code-can-the-result-keep-the-peace/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.