TypeScript - use correct version of setTimeout (node vs window)

TypescriptSettimeout

Typescript Problem Overview


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'.

Typescript Solutions


Solution 1 - Typescript

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!

Solution 2 - Typescript

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

Solution 3 - Typescript

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

Solution 4 - Typescript

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

Solution 5 - Typescript

This works perfectly well for me.

type Timer = ReturnType<typeof setTimeout>

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

Solution 6 - Typescript

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

Solution 7 - Typescript

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

Solution 8 - Typescript

I was using React and had a similar issue as well and solved it as follows:

import React, { useRef, useState, useEffect} from 'react';
import { Alert } from '../types/alerts';

const AlertComponent: React.FC<{alert: Alert}> = ({alert}) => {
  const intervalRef = useRef<NodeJS.Timeout>();
  const [count, setCount] = useState(alert.timeLimit)

  useEffect(() => {
    intervalRef.current = setInterval(
      () => {setCount((count) => count - 1)},
      1000
    )

    return () => {
      clearInterval(intervalRef.current as NodeJS.Timeout)
    }
  }, [])

  return (
    <p>{count}</p>
  )
}

export default AlertComponent;

In my useEffect() hook, I have clearInterval(intervalRef.current as NodeJS.Timeout) because clearInterval is explicitly looking for NodeJS.Timeout | undefined, so I had to get rid of the undefined portion.

Solution 9 - Typescript

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

Solution 10 - Typescript

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionKevin TigheView Question on Stackoverflow
Solution 1 - TypescriptAkxeView Answer on Stackoverflow
Solution 2 - TypescriptdhiltView Answer on Stackoverflow
Solution 3 - TypescriptcwouterView Answer on Stackoverflow
Solution 4 - TypescriptNick BernardView Answer on Stackoverflow
Solution 5 - TypescriptDedaDevView Answer on Stackoverflow
Solution 6 - TypescriptKrishnaView Answer on Stackoverflow
Solution 7 - Typescriptn9512378View Answer on Stackoverflow
Solution 8 - TypescriptShahView Answer on Stackoverflow
Solution 9 - TypescriptDanniel HanselView Answer on Stackoverflow
Solution 10 - TypescriptMark DolbyrevView Answer on Stackoverflow