Home > Enterprise >  How to convert JS function to Typescript when it has class-like variables inside of it?
How to convert JS function to Typescript when it has class-like variables inside of it?

Time:01-20

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);
        };
    });

Playground link

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);
        };
    });

Playground link


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);
// ...
  • Related