Experiments in recreating Rust's try operator in Typescript
I love neverthrow, io-ts' Either, or a homemade Result library as much as anyone, but I do miss the ?
-operator in Typescript.
This is my attempt at recreating it[1].
The basic idea
There are two core things that Rust's ?
-operator allows us to do: early return on errors and (as a result) chain Result
types as if they're always Ok
s. Let's get the first part out of the way: we cannot replicate inline early returns in Typescript.
For the second part, any Javascript aficionado might suggest that this already exists. We already have a ?
chaining operator in JS/TS. However, that works only for nullish values, which we actually want to allow.
We can sort of create a new operator though.
The spider "operator"
Attempt number one is based on Javascript's Proxy object, which allows us to override things like getters and setters. We'll build a spider operator 𐳱
[2] that allows us to operate on the potential error state of a Result type:
type MyObj = { bar: { foo: String } };
const ok1 = ok<MyObj, Error>({ bar: { foo: "it's ok" } });
const mappedOk = ok1[𐳱].bar[𐳱].foo.map(() => "it's not ok" as String);
console.assert(𐳱 in mappedOk);
console.assert(mappedOk.unwrapOk() === "it's not ok");
const error1 = err<MyObj, Error>(new Error("hello world"));
const mappedError = error1.mapErr(() => new Error("other world"))[𐳱].bar;
console.assert(𐳱 in mappedError);
console.assert(mappedError.unwrapErr().message === "other world");
I went into this expecting this to be an entirely ridiculous API, but now that I've played with it a little bit it turns out to... not be that crazy, maybe? It differs slightly from Rust: because we cannot do early returns, we have found a slightly nicer way of chaining on properties than simply chaining maps like .map(val => val.prop).map(…)
.
Let's build it! Obviously, we'll need to define the spider operator[3]:
const 𐳱 = Symbol.for("SPIDER_OPERATOR");
Then I'll define the Result
type that both the ok
and err
classes will implement:
interface Result<
// these types need to extend object, or they won't have props to access.
Ok extends object,
Err extends object
> {
map<NewOk extends object>(cb: (e: Ok) => NewOk): Result<NewOk, Err>;
mapErr<NewErr extends object>(cb: (e: Err) => NewErr): Result<Ok, NewErr>;
unwrapOk(): Ok | never;
unwrapErr(): Err | never;
[𐳱]: {
// make sure that any accessed prop is wrapped as a result also
[Prop in keyof Ok]: Ok[Prop] extends object ? Result<Ok[Prop], Err> : never;
};
}
For this blog, let's define a small part the ok
type only. The full version is linked in this Typescript playground.
function ok<Ok extends object, Err extends object>(value: Ok): Result<Ok, Err> {
return {
get [𐳱]() {
return new Proxy(
value,
{ get: (target, prop) => ok(target[prop]) }
);
},
map<NewOk extends object>(cb: (e: Ok) => NewOk): Result<NewOk, Err> {
return ok(cb(value));
},
// …
};
}
This is fairly straightforward: the function returns an object with some functions on it. The complicated bit is the get [𐳱]()
definition here. There's some special stuff going, so let's break it down into the two maybe complicated bits:
- We define a getter function for the spider prop (using computed property names):
get [𐳱]() {}
- We create a Proxy on the actual value, which changes the property getter to wrap the result in a new call to
ok(…)
:
return new Proxy(value, { get: (target, prop) => ok(target[prop]) })
- A bunch of type magic that I've omitted to get this to work.
This way, any time we try to access a property of this value, the proxy intercepts that and returns a resultified version of the value instead.
What's wrong in this picture?
Lots!
First of all, this whole thing is silly and bad. I'm doing a lot of work here to build some weird silly syntax sugar that isn't even very nice to use.
Second, as I alluded to before, there's a lot of ✨ type magic ✨ that I'm leaving out (including some naughty any
s here and there).
Most importantly because Proxy
can only work on objects, the Result has to operate on objects for now. That means it cannot work on primitive values, like strings and numbers. You still want to be able to call the spider operator on a string though, since you might want to call something like .toUpperCase()
on it. In the (more) real version on the playground I actually call a function to convert primitive values into their object equivalents (like String
and Number
).
Are there next steps?
This was an experiment! I'm not intending to make something useful out of this. But who knows, maybe I'll continue experimenting!
This is extremely experimental, the code is quite silly and probably really brittle, and I don't necessarily like it myself. Use at your own peril. ↩︎
I tooted/tweeted about this at some point.
𐳱
is a unicode character that's in the right block to be used as a Javascript variable. I'm using this because it's an experiment and I can do what I want to. ↩︎Yes, certainly we could just not use a symbol since it's a valid property name also, and we could just name the property 𐳱, and access it like
ok1.𐳱.bar.𐳱.foo.map
. I simply think the way I'm doing it looks a bit more like it's actual syntax. ↩︎