Home Toggle dark mode

JS Dark Arts: Abusing prototypes and the Result type

Let’s say we have a function from an external library that looks like this:

function defuseBomb(): boolean {
  if (Math.random() < 0.1) {
    throw new Error();
  }
  
  return true;
}

Since it might throw, we’ll have to wrap it in a try-catch block, but a better alternative would be to have the function return a Result instead of throwing:

type Result<T> = 
  | { value: T; error?: never }
  | { error: Error };

Due to the nature of results, we would be forced to check for any errors before accessing the value. Sadly though, we’re stuck with the try-catch, because the function is from an external library.

try {
  const value = defuseBomb();
} catch {}

But what if we could do this:

const result = defuseBomb.try();
//    ^ Result<boolean>

Step 1. Drawing the pentagram

We’ll add a method to all functions named try, which will execute this inside a try...catch block, converting the return value to a Result type. Just in case you skimmed the last sentence: WE WILL ADD A METHOD TO ALL FUNCTIONS.

First, we must convince TypeScript this method exists by extending the built-in Function interface:

declare global {
  interface Function {
    try<T extends (...args: any) => any>(
      this: T,
      ...args: Parameters<T>
    ): ReturnType<T> extends Promise<infer R>
      ? Promise<Result<R>>
      : Result<ReturnType<T>>;
  }
}

Next, we’ll write the new method into the function prototype.

Function.prototype.try = function <T extends (...args: any) => any>(this: T, ...args: Parameters<T>) {
  try {
    const maybe = this(...args);
    if ((maybe as any) instanceof Promise) {
      return maybe
        .then((value: ReturnType<T>) => ({ value }))
        .catch((error: Error) => ({ error }));
    } else {
      return { value: maybe };
    }
  } catch (error) {
    return { error: error };
  }
};

Step 2. Summoning the demons

Using this on the original function, gives us this:

const result = defuseBomb.try(); // This is completely safe
//    ^ Result<boolean>

const value = defuseBomb();      // This might throw an error
//    ^ boolean

And yes, this works on anything:

const res = await fetch.try("https://google.com")
//    ^ Result<Response>

const data = JSON.parse.try(`{"foo": "bar"}`)
//    ^ Result<any>

This should absolutely not be used and will mess with generics inferred from parameters, but still, its pretty neat.

Thanks for reading.