"The Wrong setTimeout": How TypeScript Drives Developers Crazy
\nHave you ever written a simple timer in TypeScript, and the code editor suddenly started underlining everything in red? A familiar scenario: you import setTimeout, write const timer = setTimeout(...), and TypeScript throws an error or suggests an unclear type. What's the matter? It turns out that setTimeout has two versions — one for the browser (window), and another for the server (Node.js). And TypeScript, being a perfectionist, forces us to choose.
This topic is one of the most popular on Stack Overflow. Thousands of developers stumble over the same rake. But we won't just copy an answer. We'll get to the root of the problem, understand how typing works, and learn to write code that won't break in either the browser or Node.js.
\n\nWhy Two Versions of setTimeout?
\nHistorically, JavaScript lives in two worlds: client-side (browser) and server-side (Node.js). In the browser, setTimeout is a method of the global window object. In Node.js, it's a function from the global timers module. They do the same thing, but their type signatures differ.
Here are the key differences:
\n- \n
- Return value: In the browser,
window.setTimeoutreturns anumber(timer identifier). In Node.js, it returns aNodeJS.Timeoutobject. \n - Context: In the browser,
thisinside the callback iswindow(in non-strict mode). In Node.js, it's the timer object. \n - Typing: TypeScript provides different declarations for different environments. If you write universal code (an isomorphic application), a conflict arises. \n
What the Problem Looks Like in Code
\nImagine you are developing a library that should work both on the client and the server. You write code like this:
\n// utils.ts\nexport function delay(ms: number): Promise<void> {\n return new Promise(resolve => {\n const timer = setTimeout(resolve, ms);\n // What type is timer? number or NodeJS.Timeout?\n });\n}\nTypeScript will see that setTimeout can be of two kinds and will throw an error: "Type 'Timeout' is not assignable to type 'number'." Or vice versa, depending on the tsconfig.json settings and the included types.
To make it clearer, here is what the types look like when viewed through ReturnType:
// In the browser (lib.dom.d.ts)\ntype BrowserTimer = ReturnType<typeof window.setTimeout>; // number\n\n// In Node.js (@types/node)\ntype NodeTimer = ReturnType<typeof setTimeout>; // NodeJS.Timeout\n\nCorrect Solutions for Different Scenarios
\nLet's look at four approaches. Choose the one that fits your project.
\n\n1. If you are writing only for the browser
\nThe simplest case. Just specify the type number and use window.setTimeout explicitly:
// browser-timer.ts\nconst timerId: number = window.setTimeout(() => {\n console.log('One second has passed');\n}, 1000);\n\n// For clearing\nwindow.clearTimeout(timerId);\nWhy does this work? In a browser environment, window.setTimeout always returns a number. TypeScript knows this if you have lib: ["dom"] included.
2. If you are writing only for Node.js
\nHere, the type is NodeJS.Timeout. Use it directly:
// node-timer.ts\nimport { setTimeout } from 'timers'; // explicit import for clarity\n\nconst timer: NodeJS.Timeout = setTimeout(() => {\n console.log('Timer fired');\n}, 2000);\n\nclearTimeout(timer); // clearTimeout accepts NodeJS.Timeout\nImportant: In Node.js, clearTimeout also accepts No