The following implementation throws an error (see comment below), how to resolve this?
interface PromiseWithAbort extends Promise<unknown> {
abort: () => void
}
export const pause = (
ms?: number,
cb?: (...args: unknown[]) => unknown,
...args: unknown[]
): PromiseWithAbort => {
let timeout
// Error: Property 'abort' is missing in type 'Promise<unknown>'
// but required in type 'PromiseWithAbort'.
const promise: PromiseWithAbort = new Promise((resolve, reject) => {
timeout = setTimeout(async () => {
try {
resolve(await cb?.(...args))
} catch (error) {
reject(error)
}
}, ms)
})
promise.abort = () => clearTimeout(timeout)
return promise
}
The problem is that the promise you're assigning to promise
doesn't have an abort
property, but one is required by the type you've assigned promise
. One simple way to fix that is to add it before assigning it to promise
. (This will also let you get rid of the explicit type on promise
.)
There are a couple of other things as well, see ***
comments:
interface PromiseWithAbort extends Promise<unknown> {
abort: () => void
}
export const pause = (
ms?: number,
cb?: (...args: unknown[]) => unknown,
...args: unknown[]
): PromiseWithAbort => {
let timeout: number; // *** Need the type in order to avoid implicit `any`
// *** Add `abort` to the promise before assigning to `promise`
const promise = Object.assign(
new Promise((resolve, reject) => {
timeout = setTimeout(async () => {
try {
resolve(await cb?.(...args));
} catch (error) {
reject(error);
}
}, ms); // *** `ms` needs a default value, you're optionally passing `undefined`
}), {
abort: () => clearTimeout(timeout)
}
);
return promise;
}
That said, using await
on the promise returned by cb
(if any) and passing the result into resolve
is a bit round-about; instead, you can just pass the promise into resolve
, which will resolve the promise you created to the one returned by cb
(if any):
export const pause = (
ms?: number,
cb?: (...args: unknown[]) => unknown,
...args: unknown[]
): PromiseWithAbort => {
let timeout: number;
// *** Add `abort` to the promise before assigning to `promise`
const promise = Object.assign(
new Promise((resolve, reject) => {
timeout = setTimeout(() => { // *** No need for `async`
try {
resolve(cb?.(...args)); // *** No need for `await`, just resolve the promise to `cb`'s promise
} catch (error) {
reject(error);
}
}, ms);
}), {
abort: () => clearTimeout(timeout)
}
);
return promise;
}
Just for what it's worth, I wouldn't add abort
to the promise, not least because when you use .then
or .catch
on that promise or use it in an async
function, the promise you get from them won't have the abort
method. Instead, you might consider accepting an AbortSignal
.
I'd also remove cb
and just make pause
a pure pausing function. cb
complicates it unnecessarily; you can just use .then
or await
and then call cb
directly in your code.
Here's an example:
class CancelledError extends Error {
constructor(msg = "Operation was cancelled") {
super(msg);
}
}
interface PauseOptions {
signal?: AbortSignal;
silent?: boolean;
}
export const pause = (
ms: number,
{signal, silent = false}: PauseOptions = {}
): Promise<void> => {
return new Promise((resolve, reject) => {
// Function we'll use if the operation is cancelled
const cancelled = () => {
if (!silent) {
reject(new CancelledError());
}
};
// The actual timer
const handle = setTimeout(() => {
if (signal?.aborted) { // It would be rare for this to happen
cancelled();
} else {
resolve();
}
}, ms);
// Handle cancellation
signal?.addEventListener("abort", () => {
clearTimeout(handle);
cancelled();
});
});
};