Trouble with TypeScript Timeouts: How to Fix It

The tl;dr is this:

You can fix the problem by replacing number with ReturnType<typeof setTimeout>

Similarly you can replace number with ReturnType<typeof setInterval> if you are seeing the problem with setInterval.

If you want to know more about it, read on.

I've run into this problem a number of times where I'm developing frontend TypeScript and after I import a package, I suddenly find throughout my project I'm receiving:

error TS2322: Type 'Timeout' is not assignable to type 'number'.

If you are working on a Node project rather than a front end project this can also manifest itself as the opposite

error TS2322: Type 'number' is not assignable to type 'Timeout'

Usually this happens because some dependency included @types/node under dependencies instead of devDependencies. When that happens, Node's global types leak into your project—even in frontend environments—causing setTimeout to return a Timeout object instead of a number.

The heart of the problem is that setTimeout() and setInterval() in Node API return a Timeout whereas in the HTML DOM Window API, they return an integer.

The ideal solution would be for the offending package to fix the problem themselves, and for you to update to the corrected version of the package. This is a hassle and might not be possible, with the issue going ignored for months if not years - as on Facebook's Jest package [1] [2].

For example, consider the following:

let x : number = setTimeout(() => console.log("foo"), 1000);

Your immediate reaction might be to either change the definition to let x : Timeout or eliminate the explicit type definition all together à la

let x = setTimeout(() => console.log("foo"), 1000);

Both of these options work.

The first option of setting it to Timeout suffers from two issues though. Firstly it's fragile - presumably you're not explicitly including @types/node so the change to the environment is a side effect you're coding around. Should the specific packages you include change, or said package correct this it will break. Second, it's ambiguous. Timeout isn’t a global type—it's specific to Node's type system—so using it assumes a Node environment even if you're working in the browser. That can confuse collaborators or tooling down the line.

The second option of implicit typing will work in a lot of cases just fine. There are many cases where you'd want for instance to define the variable in a higher scope than you are setting its value - as a trivial example

class Foo {
private x? : number;

start() {
this.x = setTimeout(() => console.log("foo"), 1000);
}

stop() {
clearTimeout(this.x);
}
}

The answer is frustratingly simple. We can just use the ReturnType utility type to set our type to the return type of setTimeout. We can do this using ReturnType<typeof setTimeout>.

Our examples from above become

let x : ReturnType<typeof setTimeout> = setTimeout(() => console.log("foo"), 1000);

and

class Foo {
private x?: ReturnType<typeof setTimeout>;

start() {
this.x = setTimeout(() => console.log("foo"), 1000);
}

stop() {
clearTimeout(this.x);
}
}

Similarly, if you are using setInterval you can utilize type ReturnType<typeof setInterval> for similar purposes.