Recipe · Node and platforms

What type does setTimeout return in JavaScript, Node.js or browser?

  • Node.js (ES modules)
  • setTimeout
  • ReturnType
  • @types/node
  • Checked with TypeScript 5.9

The problem I store a timer in a variable typed as a number, and tsc says Timeout is not assignable to number. The same code passes in my browser project. What is the right type for a timer handle?

The short answer

Type the handle as ReturnType<typeof setTimeout>. It is whatever setTimeout returns in your project, so the same code checks under Node.js types and browser types:

autosave.js autosave.ts TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
/** @type {ReturnType<typeof setTimeout> | undefined} */
let pending;

/**
 * @param {() => void} save
 * @param {number} [delay]
 */
export function scheduleSave(save, delay = 500) {
  if (pending !== undefined) clearTimeout(pending);
  pending = setTimeout(() => {
    pending = undefined;
    save();
  }, delay);
}

scheduleSave(() => console.log("saved"));
let pending: ReturnType<typeof setTimeout> | undefined;

export function scheduleSave(save: () => void, delay = 500): void {
  if (pending !== undefined) clearTimeout(pending);
  pending = setTimeout(() => {
    pending = undefined;
    save();
  }, delay);
}

scheduleSave(() => console.log("saved"));
A timer handle typed with ReturnType<typeof setTimeout>.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": ["node"]
  },
  "include": ["*.js"]
}

Why it works

Two different setTimeout declarations exist. TypeScript’s DOM library declares the browser’s, which returns a number. @types/node declares Node.js’s, which returns a NodeJS.Timeout object (with methods such as unref()). ReturnType<typeof setTimeout> asks the checker for the return type of the declaration your config loads, so you never have to choose.

The same thing in TypeScript

The TypeScript equivalent tab writes let pending: ReturnType<typeof setTimeout> | undefined, the same type in annotation position.

What goes wrong

In a project with @types/node, a number annotation does not fit the Timeout object (TS2322):

autosave.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: autosave.js(8,3): error TS2322: Type 'Timeout' is not assignable to type 'number'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. autosave.js(8,3): error TS2322: Type 'Timeout' is not assignable to type 'number'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. autosave.js(8,3): error TS2322: Type 'Timeout' is not assignable to type 'number'. checked 21 October 2026
/** @type {number | undefined} */
let pending;

/** @param {() => void} save */
export function scheduleSave(save) {
  if (pending !== undefined) clearTimeout(pending);
  pending = setTimeout(save, 500);error TS2322: Type 'Timeout' is not assignable to type 'number'.
}
In a project with Node.js types, a timer is not a number.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": ["node"]
  },
  "include": ["*.js"]
}

The same annotation passes in a browser-only project, which is why code copied between the two breaks:

autosave.js TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
/** @type {number | undefined} */
let pending;

/** @param {() => void} save */
export function scheduleSave(save) {
  if (pending !== undefined) clearTimeout(pending);
  pending = setTimeout(save, 500);
}
The same number annotation in a browser-only project passes.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "lib": ["es2022", "dom", "dom.iterable"],
    "types": []
  },
  "include": ["*.js"]
}

Gotchas

  • Projects with both type sets get Node.js’s timers. Tests that load @types/node alongside the DOM library see the Timeout return type; ReturnType<typeof setTimeout> follows that too.
  • window.setTimeout is always the browser’s. Calling it through window returns a number even when @types/node is loaded, a common way to opt into numbers in front-end code.
  • unref() is Node.js only. The Timeout object’s unref() lets the process exit while the timer is pending; browsers have no equivalent.

Questions

Can I still use the timer as a number in Node.js?

A Node.js Timeout object converts to a number (+timer gives its id), and clearTimeout accepts either. Keeping the handle as ReturnType<typeof setTimeout> avoids the conversion altogether.

What about setInterval?

The same pattern: ReturnType<typeof setInterval>, cleared with clearInterval. In Node.js both return Timeout objects.

Sources

Last reviewed 21 October 2026.