Home > Net >  How to type return type to be a promise only if receiving function also returns a promise?
How to type return type to be a promise only if receiving function also returns a promise?

Time:04-16

I'm making this silly function that will make a try/catch for any function and return it as a tuple with data and error

export type CatchReturn<T> = [data: T | undefined, error: unknown | undefined];

export const tcatch = <T>(
  tryFunc: (() => T) | (() => Promise<T>)
) => {
  try {
    const res = tryFunc();
    if (res instanceof Promise) {
      return res
        .then<CatchReturn<T>>((r) => [r, undefined])
        .catch<CatchReturn<T>>((e) => [undefined, e]);
    } else {
      return [res, undefined] as CatchReturn<T>;
    }
  } catch (e) {
    return [undefined, e] as CatchReturn<T>;
  }
};

The function works as expected, but what I'm not able to do is correctly type the return type.

At the moment, the return type is CatchReturn<T> | Promise<CatchReturn<T>> but I wanted the return type to only be CatchReturn<T> if tryFunc: (() => T) and Promise<CatchReturn<T>> only if tryFunc: (() => Promise<T>).

So for example:

How it's working at the moment

const syncRes = tcatch(() => {}) // return type is CatchReturn<T> | Promise<CatchReturn<T>>
const asyncRes = tcatch(async () => {}) // return type is CatchReturn<T> | Promise<CatchReturn<T>>

How I expect it to work

const syncRes = tcatch(() => {}) // return type should be CatchReturn<T>
const asyncRes = tcatch(async () => {}) // return type should be Promise<CatchReturn<T>>

I tried using conditional types in the return type, but always got errors and I'm not 100% sure if this is even possible.

CodePudding user response:

This is a job for function overloads. You simply declare one function signature for promises, and another for other functions. Then your implementation returns a union of both:

// signatures
export function tcatch<T>(tryFunc: () => Promise<T>): Promise<CatchReturn<T>>
export function tcatch<T>(tryFunc: () => T): CatchReturn<T>

// implementation
export function tcatch<T>(
  tryFunc: (() => T) | (() => Promise<T>)
): CatchReturn<T> | Promise<CatchReturn<T>> {
  //...
}

Which seems to work as you expect:

const syncRes = tcatch(() => {}) // CatchReturn<void>
const asyncRes = tcatch(async () => {}) // Promise<CatchReturn<void>>

Playground

CodePudding user response:

Not so elegant way, but...

export type CatchReturn<T> = [data: T | undefined, error: unknown | undefined];

type MaybeMappedPromise<F extends () => unknown> = ReturnType<F> extends Promise<infer T> ? Promise<CatchReturn<T>> : CatchReturn<ReturnType<F>>

function tcatch<T extends unknown, F extends (() => T) | (() => Promise<T>)>(
  tryFunc: F
): MaybeMappedPromise<F> {
  try {
    const res = tryFunc();
    if (res instanceof Promise) {
      return res
        .then<CatchReturn<T>>((r) => [r, undefined])
        .catch<CatchReturn<T>>((e) => [undefined, e]) as MaybeMappedPromise<F>;
    } else {
      return [res, undefined] as MaybeMappedPromise<F>;
    }
  } catch (e) {
    return [undefined, e] as MaybeMappedPromise<F>;
  }
}

const syncRes = tcatch(() => {}) // return type should be CatchReturn<T>
const asyncRes = tcatch(async () => {}) // return type should be Promise<CatchReturn<T>>
  • Related