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