448

I am working on upgrading some old TypeScript code to use the latest compiler version, and I'm having trouble with a call to setTimeout. The code expects to call the browser's setTimeout function which returns a number:

setTimeout(handler: (...args: any[]) => void, timeout: number): number;

However, the compiler is resolving this to the node implementation instead, which returns a NodeJS.Timer:

setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): NodeJS.Timer;

This code does not run in node, but the node typings are getting pulled in as a dependency to something else (not sure what).

How can I instruct the compiler to pick the version of setTimeout that I want?

Here is the code in question:

let n: number;
n = setTimeout(function () { /* snip */  }, 500);

This produces the compiler error:

TS2322: Type 'Timer' is not assignable to type 'number'.

7
  • 2
    Do you have a types:["node"] in your tsconfig.json? See stackoverflow.com/questions/42940954/… Commented Aug 21, 2017 at 20:29
  • @koe No, i don't have the types:["node"] option in the tsconfig file. But the node types are getting pulled in as an npm dependency to something else. Commented Aug 22, 2017 at 12:52
  • 4
    You could also explicitly define "types" in tsconfig.json - when you omit "node" it isn't used in compilation. e.g. "types": ["jQuery"] Commented Aug 23, 2017 at 5:48
  • 3
    It is surprising that the answer of @koe (use "types" option) doesn't have any votes, being the only true correct answer. Commented Jul 21, 2020 at 18:44
  • 2
    @KevinTighe's types doesn't include node but setTimeout still gets its Node type rather than its browser type. types defaults to all the types in node_modules/@types, as explained in typescriptlang.org/tsconfig#types, but even if you do specify types and don't include "node", why does setTimeout still get its Node type and how can you get the browser type? @Axke's solution is a bit of a hack, basically saying it returns what it returns. TypeScript may still be finding the wrong type, but at least it will be consistently wrong. Commented Sep 9, 2020 at 18:29

12 Answers 12

676
+250
let timer: ReturnType<typeof setTimeout> = setTimeout(() => { ... });

clearTimeout(timer);

By using ReturnType<fn> you are getting independence from platform. You won't be forced to use neither any nor window.setTimeout which will break if you run the code on nodeJS server (eg. server-side rendered page).


Good news, this is also compatible with Deno!

Sign up to request clarification or add additional context in comments.

8 Comments

My understanding is that this is the right answer and should be the accepted one, since it provides the right type definition for every platform supporting setTimeout / clearTimeout and does not use any.
This is the solution if you are writing a library that runs on both NodeJS and browser.
The return type is NodeJS.Timeout if using setTimeout directly and number if using window.setTimeout. Shouldn't need to use ReturnType.
@cchamberlain You need it as you run the setTimeout function and are expecting its result to be stored in the variable. Try it yourself in TS playground.
This solution works correctly for me. Without using this, my node app compiles correctly with TS, but when using Jest unit tests it chooses the incorrect window.setTimeout definition
|
310

2021 update

Akxe's answer suggests ReturnType<Type> technique introduced in Typescript 2.3:

let n: ReturnType<typeof setTimeout>;
n = setTimeout(cb, 500);

It is nice and seems to be preferred over explicit casting. But the result type of "n" in this case is "NodeJS.Timeout", and it is possible to use it as follows:

let n: NodeJS.Timeout;
n = setTimeout(cb, 500);

The only problem with ReturnType/NodeJS.Timeout approach is that numeric operations in browser-specific environment still require casting:

if ((n as unknown as number) % 2 === 0) {
  clearTimeout(n);
}

Original answer

A workaround that does not affect variable declaration:

let n: number;
n = setTimeout(function () { /* snip */  }, 500) as unknown as number;

Also, in browser-specific environment it is possible to use window object with no casting:

let n: number;
n = window.setTimeout(function () { /* snip */  }, 500);

8 Comments

I think the other one (window.setTimeout) should be the correct answer for this question as it is the clearest solution.
If you are using the any type, you aren't really giving a TypeScript answer.
likewise the number type will lead to TypeScript specific lint errors, as the setTimeout function requires more than that.
window.setTimeout may cause issues with unit test frameworks (node.js). The best solution is to use let n: NodeJS.Timeout and n = setTimeout.
@AntonOfTheWoods you should be able to scope it again, but with self instead of window stackoverflow.com/questions/57172951/… . Hopefully properly set up typescript will assign a proper type to it, but I have no experience with it.
|
50

I guess it depends on where you will be running your code.

If your runtime target is server side Node JS, use:

let timeout: NodeJS.Timeout;
global.clearTimeout(timeout);

If your runtime target is a browser, use:

let timeout: number;
window.clearTimeout(timeout);

1 Comment

What if it's isomorphic?
25

This works perfectly well for me.

type Timer = ReturnType<typeof setTimeout>

const timer: Timer = setTimeout(() => {}, 1000)

2 Comments

Basically the same as stackoverflow.com/a/56239226/3484824 which was posted almost 2 years earlier
@CanRau but for me it was still useful. Don't know why, but I failed to get the idea from the other answer, though, as I look at it now, it's not that voluminous.
14

This will likely work with older versions, but with TypeScript version ^3.5.3 and Node.js version ^10.15.3, you should be able to import the Node-specific functions from the Timers module, i.e.:

import { setTimeout } from 'timers';

That will return an instance of Timeout of type NodeJS.Timeout that you can pass to clearTimeout:

import { clearTimeout, setTimeout } from 'timers';

const timeout: NodeJS.Timeout = setTimeout(function () { /* snip */  }, 500);

clearTimeout(timeout);

1 Comment

Similarly, if you want the browser version of setTimeout, something like const { setTimeout } = window will clear up those errors.
13

Wanted to mention as well that the spec for NodeJS.Timeout includes [Symbol.toPrimitive](): number:

interface Timeout extends Timer {
    /**
     * If true, the `Timeout` object will keep the Node.js event loop active.
     * @since v11.0.0
     */
    hasRef(): boolean;
    /**
     * Sets the timer's start time to the current time, and reschedules the timer to
     * call its callback at the previously specified duration adjusted to the current
     * time. This is useful for refreshing a timer without allocating a new
     * JavaScript object.
     *
     * Using this on a timer that has already called its callback will reactivate the
     * timer.
     * @since v10.2.0
     * @return a reference to `timeout`
     */
    refresh(): this;
    [Symbol.toPrimitive](): number;
}

And for compatibility, the other timeout APIs in Node work just fine with the plain integer ids, they don't need to accept the object. The objects are used "server"-side to allow some finer control over keeping the process alive and garbage collection stuff. For example:

function clearTimeout(timeoutId: NodeJS.Timeout | string | number | undefined): void;

This means you can use a primitive cast on the result of setTimeout and setInterval:

let timeoutId: number | undefined;
timeoutId = Number(setTimeout(callback, ms));

function clear() {
  clearTimeout(timeoutId);
}

Doesn't conflict with either API, while not running you into type trouble later on if you needed to depend on it being a primitive value for some other API contract.

5 Comments

thanks, Number() helped me to convert to number
This has a runtime cost, so we shouldn't do this.
@Ashitaka ...if you develop an application where you have measured that this actually matters.
Writing code that runs at runtime to workaround a type issue instead of fixing that type issue is a no-no in my book. Even if I can't measure it.
Having not measured any impact either, I am inclined to agree with Ashitaka. Though I wanted to put forth an alternative answer others hadn't suggested, these days my preference is to use let timeoutId: ReturnType<typeof setTimeout> | undefined; which IMO does a better job of conveying the intention of the variable anyways.
5

If your code is not running in node and the node types are coming from a dependency.

  • install @types/web if you haven't already

  • create a file in your project e.g. web-types.d.ts

  • at the top of the file add the line /// <reference types="web" />

  • in your tsconfig under compilerOptions add "typeRoots": ["./web-types", "./node_modules/@types"]

This should prioritize browser types over node types.

2 Comments

` // <reference types="web" /> ` is showing an error on 'web' "Cannot find type definition file for 'web'" I installed @type/web via npm install @typescript/lib-dom@npm:@types/web --save-dev
adding @types/web solved it straight away.
1

If you're targeting setInterval of window. Then you can also write it as

let timerId: number = setInterval((()=>{
    this.populateGrid(true)
  }) as TimerHandler, 5*1000)
}

Comments

0

I was testing my Counter app using RTL and specifically was testing an element to be removed if count reaches 15. Since the component gets destroyed after running the test, setTimeout would still run after that and throw the error saying that React can't perform a state update on unmounted component. So, based on dhilt's answer, I was able to fix my useEffect cleanup function this way:

const [count, setCount] = useState(initialCount);
const [bigSize, setBigSize] = useState(initialCount >= 15);

useEffect(() => {
    let id: NodeJS.Timeout;

    if(count >= 15) {
        id = setTimeout(() => setBigSize(true), 300);
    }

    return function cleanup() {
        clearTimeout(id);
    }
});

And here's the test suite:

describe('when the incrementor changes to 5 and "add" button is clicked', () => {
        beforeEach(async () => {
            userEvent.type(screen.getByLabelText(/Incrementor/), '{selectall}5');
            userEvent.click(screen.getByRole('button', {name: "Add to Counter"}));
            await screen.findByText('Current Count: 15');
        })
            
        it('renders Current Count: 15', () => {
            expect(screen.getByText('Current Count: 15')).toBeInTheDocument();
        });
        
        it('renders too big and will dissapear after 300ms',async() => {
            await waitForElementToBeRemoved(() => screen.queryByText(/size: small/i))
        });
        
    })

Comments

-2

I faced the same problem and the workaround our team decided to use, was just to use "any" for the timer type. E.g.:

let n: any;
n = setTimeout(function () { /* snip */  }, 500);

It will work with both implementations of setTimeout/setInterval/clearTimeout/clearInterval methods.

9 Comments

Yeah, that does work. I also realized that I can just specify the method on the window object directly: window.setTimeout(...). Not sure if that's the best way to go but I'll stick with it for now.
You can import the NodeJS namespace properly in typescript, see this answer.
use of any is generally not the way to go.
Yes but properly typed and and working is the best yet.
@avalanche1 I kind of agree, but this is the case when I prefer avoiding "perfect theoretical" solutions and do "working practical" things. So instead of spending a lot of time figuring out the right typings and / or making the code hard to read by using complex Unions or similar approaches, we just make it work and go forward. If at any point in time there is a simple and easy-to-use solution, we just replace the "not-perfect-but-working" solution with the new better one.
|
-2

I solved this problem by setting

tsconfig.json:
{
  "compilerOptions": {
    "skipLibCheck": true,
  }
}

And create .d.ts

*.d.ts:
declare namespace NodeJS {
    type Timeout = number;
    type Timer = number;
}

typescript version 4.2.3

Comments

-2

TS2322: Type 'Timer' is not assignable to type 'number'.

The simple solution

abc : any;

for assgin

abc = setInterval or abc = setTimeout

1 Comment

As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.