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.
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?
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;
}
}