I'm converting a codebase to typescript and I've run into something that I have never seen before. I'm converting two functions which seem to have class-like variables in them. One of the functions in question look like this:
const wait = (ms) =>
new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
delete wait.reject;
resolve();
}, ms);
wait.reject = (reason) => {
clearTimeout(timeoutId);
reject(reason);
};
});
As you can see, it has a variable within it called wait.reject
which is an arrow function that is defined at the bottom. At the top, the reject variable is deleted after a certain period of time.
To type this, I have had to resort to writing (wait as { reject: ... }.reject
as you can see below:
const wait = (ms: number) =>
new Promise<void>((resolve, reject) => {
const timeoutId = setTimeout(() => {
delete (wait as { reject?: () => void }).reject;
resolve();
}, ms);
(wait as { reject?: (reason: string) => void }).reject = (reason: string) => {
clearTimeout(timeoutId);
reject(reason);
};
});
Of course, using as
is far from ideal. Does anybody know how to type this correctly? There is more than one function in the codebase that has this problem.
Please and thank you in advance! :)
CodePudding user response:
Preface: I would strongly recommend not having the wait
function work the way it works in that code. What if you make two calls to wait
? Then you have the second reject
overwriting the first reject
. Perhaps consider having the function return an object with promise
and reject
properties, so the consumer of the function can manage them. (Example at the end of the answer.) Of course, if you're adding types to an existing codebase you may not be able to refactor right now (but one of the great things about TypeScript, IMHO, is how much it helps when you do have time to refactor).
But to the types:
Your wait
function is a function accepting a number
parameter and returning Promise<void>
, which also has an optional reject
property which is a function which accepts an any
parameter and returns void
. (Note: That's not a "class-like variable," it's an object property. All functions are objects in JavaScript and TypeScript.) You write that type with an intersection (&
) of a function type and an object type with an optional reject
property:
type WaitFn = ((ms: number) => Promise<void>) & { reject?: (reason: any) => void };
Then use that type on wait
:
const wait: WaitFn = (ms: number) =>
// ^^^^^^^^
new Promise<void>((resolve, reject) => {
const timeoutId = setTimeout(() => {
delete wait.reject;
resolve();
}, ms);
wait.reject = (reason) => {
clearTimeout(timeoutId);
reject(reason);
};
});
In the above, I also added a type annotation on ms
(ms: number
) and a type argument (<void>
) to new Promise
, but you don't actually have to, you can allow those types to be inferred from WaitFn
if you like. Here I've removed them:
const wait: WaitFn = (ms) =>
new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
delete wait.reject;
resolve();
}, ms);
wait.reject = (reason) => {
clearTimeout(timeoutId);
reject(reason);
};
});
But as I said at the top, I wouldn't write wait
that way, because two overlapping calls to it have cross-talk — the function ends up with the second reject
on it.
Instead, I'd probably return an object with promise
and reject
members:
const wait = (ms: number) => {
let promiseReject: (reason: any) => void;
let timeoutId: ReturnType<typeof setTimeout>;
const promise = new Promise<void>((resolve, _reject) => {
promiseReject = _reject;
timeoutId = setTimeout(() => {
resolve();
}, ms);
});
let reject = (reason: any) => {
clearTimeout(timeoutId);
promiseReject(reason);
};
return { promise, reject };
};
You'd use it by receiving those, then waiting for the promise, calling reject
if appropriate:
const { promise, reject } = wait(1000);
// ...