swan-io / boxed

Essential building-blocks for functional & safe TypeScript code

Home Page:https://swan-io.github.io/boxed

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to detect is a Future or Result is instanceof Future or Result

thuringia opened this issue · comments

Hi,

thanks for building this library, so far it has been my team's life much easier!
For performance tracking I have to find out, how long it takes for a function to execute. That part is not the problem, unfortunately our code is a mixture of return values, and tracking Future, Result and Promise requires type-specific code.

It looks like using instanceof cannot be used here, as can be seen in this Replit. So I created these type guards, which while serviceable are rather hacky:

type AnyFunction = (...args: any[]) => any;

const isFuture = (x: unknown): x is Future<any> =>
  Reflect.has((x as AnyFunction).constructor, "value");

const isResult = (x: unknown): x is Result<any, any> =>
  Reflect.has((x as AnyFunction).constructor, "isOk");

Is there a better solution for this?

Thank you for any help or tips and I hope you have a great day!
Robert

@thuringia Hi!

As you might know, we highly recommend ts-pattern, a super cool 1kb pattern matching library. Boxed even offers interop with it.

This library also gives you isMatching, a nice util to create type guards.
I personally would use it like this:

import { Future, Result } from "@swan-io/boxed";
import { P, isMatching, match } from "ts-pattern";

// First, we grab a Future constructor:
const { constructor } = Future.make(() => {});

// Then we create type guards:
const isFuture = (value: unknown): value is Future<unknown> =>
  isMatching({ constructor }, value); // match if value has an identical constructor

const isPromise = (value: unknown): value is Promise<unknown> =>
  isMatching(P.instanceOf(Promise), value); // match if value is an instance of Promise

const isResult = (value: unknown): value is Result<unknown, unknown> =>
  isMatching(P.union(Result.P.Ok(P._), Result.P.Error(P._)), value); // match if any of the Result provided patterns match

const x = Future.make(resolve => {
  const timeoutId = setTimeout(() => {
    resolve(0);
  }, 1000);

  return () => clearTimeout(timeoutId);
});

const y = Promise.resolve(1);
const z = Result.Ok(2);

// All good!
console.log("Future checks");
console.log("isFuture", isFuture(x));

console.log("\nPromise checks");
console.log("isPromise", isPromise(y));

console.log("\nResult checks");
console.log("isResult", isResult(z));

// value could be a Future | Promise | Result
const value = Math.random() > 0.5 ? x : Math.random() > 0.5 ? y : z;

if (isFuture(value)) {
  value; // value type is narrowed to Future
}
if (isPromise(value)) {
  value; // value type is narrowed to Promise
}
if (isResult(value)) {
  value; // value type is narrowed to Result
}

// ts-pattern match can also be used:
match(value)
  .with(P.when(isFuture), value => value) // value is Future
  .with(P.when(isPromise), value => value) // value is Promise
  .with(P.when(isResult), value => value) // value is Result
  .otherwise(() => {});

EDIT: without ts-pattern (less elegant IMHO)

const future = Future.make(() => {});

// match if value has an identical constructor
const isFuture = (value: unknown): value is Future<unknown> =>
  value != null && Object.is(value.constructor, future.constructor);

// match if value is an instance of Promise
const isPromise = (value: unknown): value is Promise<unknown> => value instanceof Promise;

// match if any of the Result provided patterns match
const isResult = (value: unknown): value is Result<unknown, unknown> =>
  value instanceof Object &&
  "tag" in value &&
  // If the internal property name change, TS will error as tag property will not exists
  (value.tag === Result.P.Ok(null).tag || value.tag === Result.P.Error(null).tag);

Maybe we could ship these guards directly with the library (well, except the Promise one of course 😄)

@zoontek Using the whole constructor object is a much better idea.
This is very elegant with ts-pattern as well. We already are heavy users of ts-pattern, so that approach is right up our alley. Thanks for that tip!

I think shipping type guards as part of the library is an excellent idea. I can create a PR for this if you want, including additional ones like isDeferred or isOption. The biggest question here is, if they should use ts-pattern here, as that would introduce a hard dependency, instead of just interop.

v1.1.0 includes the following guards:

  • Option.isOption
  • Result.isResult
  • AsyncData.isAsyncData
  • Future.isFuture