Check type of a function parameter

203 views Asked by At

Since TypeScript is a superset of Javascript and 'Type' would be removed in the resulting js files, so I guess playing with 'type' would not work like this, right? Also, is there any better way to do it other than having two different methods?

type VoidToVoidFunc = () => void;
type VoidToPromiseVoidFunc = () => Promise<void>;

async function add(func: VoidToVoidFunc  | VoidToPromiseVoidFunc) {
    if (typeof func == typeof VoidToVoidFunc)
        func(); 
    else
        await func();
}

add(() => console.log(1));
4

There are 4 answers

0
Guerric P On BEST ANSWER

As you can see here, when a non-thenable value follows await, an already-fulfilled Promise is constructed and used.

So the simplest implementation is to await the result of the function call. Indeed, await wraps a value in a Promise if it's not already a Promise:

type VoidToVoidFunc = () => void;
type VoidToPromiseVoidFunc = () => Promise<void>;

async function add(func: VoidToVoidFunc  | VoidToPromiseVoidFunc) {
  await func();
  // Do something else
}

add(() => console.log(1));

This behavior is correctly typed by TypeScript, indeed when you write const test = await func(); and hover the variable, its type is correctly inferred to void.

TypeScript playground

5
Nicolas On

You're right, you cannot rely on the type since it's going to be removed once compiled to JavaScript.

However, you can check the return value of your function. With that, you can check if you should await the promise or simply resolve the value.

type VoidToVoidFunc = () => void;
type VoidToPromiseVoidFunc = () => Promise<void>;

async function add(func: VoidToVoidFunc  | VoidToPromiseVoidFunc) {
    /* is either a pending promise or undefined */
    const value = func();

    if(value instanceof Promise) {
      await value;
    }
}

In the case where your function returns a promise, you are await-ing it. And in the case it does not, you are simply skipping the await part.

The following example is in plain JavaScript, to show you how it works.

async function add(func) {
    /* is either a pending promise or undefined */
    const value = func();

    if(value instanceof Promise) {
      await value;
    }
}

const asyncFunction = async () => {
  return new Promise(resolve => {
    setTimeout(() => {
        console.log('resolving promised function');
        resolve();
    }, 1000)
  });
};

const syncFunction = () => {
    console.log('resolving normal function')
}

add(asyncFunction);

add(syncFunction);

0
Aluan Haddad On

The cleanest and most maintainable solution is the most strait forward

type VoidToVoidFunc = () => void;
type VoidToPromiseVoidFunc = () => Promise<void>;

async function add(func: VoidToVoidFunc | VoidToPromiseVoidFunc) {
  await func();
}

add(() => console.log("sync function", 1));

add(async () => {
  await delay(1000);
  console.log("async function", 2);
});


function delay(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

Playground Link

async function add(func) {
  await func();
}

add(() => console.log("sync function", 1));

add(async () => {
  await delay(1000);
  console.log("async function", 2);
});

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

As you can see from the above, behavior, applying await to a non Promise value simply wraps and unwraps it. As your add function by virtue of being denoted async is necessarily going to return a Promise and run asynchronously (scheduled on the next tick), more complex solutions offer no benefit in your case.

Check out the async function and Promise docs at MDN for further elaboration.

Thus, while there are indeed other ways to determine if a you received a callback that may or may not return a Promise, such as

type VoidToVoidFunc = () => void;
type VoidToPromiseVoidFunc = () => Promise<void>;

async function add(func: VoidToVoidFunc | VoidToPromiseVoidFunc) {
  const couldBeAPromise = func();
  if (couldBeAPromise && 'then' in couldBeAPromise) {
    await couldBeAPromise;
  }
}

add(() => console.log("sync function", 1));

add(async () => {
  await delay(1000);
  console.log("async function", 2);
});


function delay(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

Playground Link

async function add(func) {
  const couldBeAPromise = func();
  if (couldBeAPromise && 'then' in couldBeAPromise) {
    await couldBeAPromise;
  }
}

add(() => console.log("sync function", 1));

add(async () => {
  await delay(1000);
  console.log("async function", 2);
});

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

keeping it simple is the way to go here.

Note: void is a TypeScript type expressing the contract of a function and guiding type inference. However, every JavaScript function and thus every TypeScript function actually returns a value. In this case func param of add returns either undefined or a Promise that resolves to undefined.

0
Chukwujiobi Canon On

await does not require its operand to be a promise! It returns the value of the awaited expression if it is not a promise.

await expression

expression
A Promise, a thenable object, or any value to wait for.

Reference: MDN

So a synchronous subroutine is allowed. With this in mind, simplify your code as follows:

type VoidToVoidFunc = () => void;
type VoidToPromiseVoidFunc = () => Promise;

async function add(func: VoidToVoidFunc  | VoidToPromiseVoidFunc) {
    await func(); /* <—- valid whether func() is synchronous or not. */
}

add(() => console.log(1));