Search code examples
typescriptgenerics

Duck type a Promise in Typescript


Note to Future Readers

There is a built-in interface in the global Typescript typings for generic thenables called PromiseLike, and you should use that for the generic constraint.

Original Question:

Let's say I have a logging function that takes a function and logs the name, arguments, and result:

function log<A extends any[], B>(f: (...args: A) => B): (...args: A) => B {
    return function (...args: A): B {
        console.log(f.name);
        console.log(JSON.stringify(args));
        const result = f(...args);
        console.log(result);
        return result;
    }
}

This works, and AFAICT preserves the type-safety of the passed-in function. But this breaks if I want to add special handling for Promises:

function log<A extends any[], B>(f: (...args: A) => B) {
    return function (...args: A): B {
        console.log(f.name);
        console.log(JSON.stringify(args));
        const result = f(...args);
        if (result && typeof result.then === 'function') {
            result.then(console.log).catch(console.error);
        } else {
            console.log(result);
        }
        return result;
    }
}

Here the compiler complains that .then does not exist on type B. So I can cast to a Promise:

if (typeof (<Promise<any>>result).then === 'function') {

This too does not work, and it is a less-specific type than the generic one. The error message suggests converting to unknown:

const result: unknown = f(...args);

But now the returned type doesn't match the return signature of the HOF, and the compiler naturally won't allow it.

Now, I can use a check for instanceof Promise:

if (result instanceof Promise) {
  result.then(console.log).catch(console.error);

and the compiler is happy. But this is less than ideal: I'd prefer to do a generic test for any thenable rather than just the native Promise constructor (not to mention any oddball scenarios like the Promise constructor coming from a different window). I'd also prefer to have this be one function rather than two (or more!). And indeed, using this check to determine if a method exists on an object is a pretty common Javascript idiom.

How do I do this while preserving the return type of the original function parameter?


Solution

  • I'd prefer to do a generic test for any thenable rather than just the native Promise constructor

    This might meet your requirements:

    if (result && typeof (result as any).then === 'function') {
      (result as any).then(console.log).catch(console.error);
    } else {
      console.log(result);
    }
    

    If it does, you could factor it into a user-defined type guard:

    const isThenable = (input: any): input is Promise<any> => { 
      return input && typeof input.then === 'function';
    }
    

    With the user-defined type guard, the log function would look like this (here it is in the TypeScript playground):

    const isThenable = (input: any): input is Promise<any> => { 
      return input && typeof input.then === 'function';
    }
    
    function log<A extends any[], B>(f: (...args: A) => B) {
      return function (...args: A): B {
        console.log(f.name);
        console.log(JSON.stringify(args));
        const result = f(...args);
        if (isThenable(result)) {
          result.then(console.log).catch(console.error);
        } else {
          console.log(result);
        }
        return result;
      }
    }