Question
I am upgrading older TypeScript code to a newer compiler version and ran into a problem with setTimeout.
My code is intended to run in the browser, where setTimeout returns a number:
setTimeout(handler: (...args: any[]) => void, timeout: number): number;
However, TypeScript is resolving setTimeout to the Node.js version instead, which returns a NodeJS.Timer (or Node-specific timeout object):
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): NodeJS.Timer;
This code does not run in Node.js, but Node typings are being included indirectly through another dependency.
Here is the code that causes the issue:
let n: number;
n = setTimeout(function () {
/* snip */
}, 500);
TypeScript reports this error:
TS2322: Type 'Timer' is not assignable to type 'number'.
How can I tell TypeScript to use the browser version of setTimeout, or otherwise write this code correctly?
Short Answer
By the end of this page, you will understand why setTimeout has different return types in browser and Node.js environments, why TypeScript sometimes picks the Node version unexpectedly, and how to write timeout code that works safely in browser-only or mixed TypeScript projects.
Concept
TypeScript does not invent APIs like setTimeout; it reads their types from declaration files provided by the active environment.
For setTimeout, the return type depends on where your code is expected to run:
- Browser:
setTimeoutreturns a numeric timer ID. - Node.js:
setTimeoutreturns a timeout object.
This becomes confusing when a project includes both DOM types and Node types. If Node typings are present, TypeScript may treat the global setTimeout as the Node version or as an overloaded function with multiple possible return types.
That is why this code can fail:
let n: number;
n = setTimeout(() => {}, 500);
If TypeScript sees the Node definition, the returned value is not a number, so assignment fails.
This matters in real projects because many frontend builds accidentally include @types/node through tooling, test libraries, or bundler dependencies. Once those types are visible, globals such as setTimeout, clearTimeout, process, and may affect your type checking.
Mental Model
Think of setTimeout as a function with the same name living in two different buildings:
- In the browser building, the receptionist hands you a number ticket.
- In the Node.js building, the receptionist hands you an object pass.
Your code says, "I expect a number ticket," but TypeScript looks around and says, "I also see the Node building, so this might be an object pass instead."
The fix is to either:
- clearly walk into the browser building using
window.setTimeout, or - stop assuming the ticket is always a number and let TypeScript infer the correct return type.
Syntax and Examples
The most common ways to handle this are below.
1. Browser-only code: use window.setTimeout
If your code definitely runs in the browser, reference the browser global directly:
let n: number;
n = window.setTimeout(() => {
console.log("Runs after 500ms");
}, 500);
Why this works
window.setTimeout comes from the DOM typings, so TypeScript uses the browser signature, which returns a number.
2. Environment-safe code: use ReturnType<typeof setTimeout>
If you want code that works whether TypeScript sees browser or Node typings, do this:
let timer: ReturnType<typeof setTimeout>;
timer = setTimeout(() => {
console.log("Runs after 500ms");
}, 500);
Step by Step Execution
Consider this example:
let timer: ReturnType<typeof setTimeout>;
timer = setTimeout(() => {
console.log("Done");
}, 1000);
clearTimeout(timer);
Here is what happens step by step:
-
let timer: ReturnType<typeof setTimeout>;- TypeScript checks the current type of
setTimeout. - It computes the type that this function returns.
- In a browser, this is usually
number. - In Node.js, this is usually a timeout object.
- TypeScript checks the current type of
-
timer = setTimeout(() => { ... }, 1000);- A timeout is scheduled.
- The returned handle is stored in
timer. - The assignment is valid because
timerwas typed from the actual function.
-
clearTimeout(timer);- The scheduled timeout is cancelled.
Real World Use Cases
Timeout handles appear in many common programming tasks:
Delayed UI actions
window.setTimeout(() => {
modal.classList.add("visible");
}, 300);
Used for animations, tooltips, banners, and delayed notifications.
Debouncing user input
let timer: ReturnType<typeof setTimeout>;
function onSearchInput() {
clearTimeout(timer);
timer = setTimeout(() => {
console.log("Send API request");
}, 300);
}
Used in search boxes, validation, and autosave features.
Retrying operations
setTimeout(() => {
console.log("Retry request");
}, 1000);
Used after temporary API failures.
Real Codebase Usage
In real codebases, developers usually use one of these patterns.
Pattern 1: Explicit browser global
Used when code is clearly browser-only:
const id = window.setTimeout(() => {
console.log("browser only");
}, 500);
This is common in UI components, browser widgets, and DOM event handlers.
Pattern 2: Environment-agnostic timer type
Used in shared libraries, React apps, testable utilities, and code that may be compiled with both DOM and Node typings:
let timeoutRef: ReturnType<typeof setTimeout> | null = null;
function scheduleWork() {
if (timeoutRef !== null) {
clearTimeout(timeoutRef);
}
timeoutRef = setTimeout(() => {
console.log("work done");
}, 250);
}
This pattern is especially common for:
Common Mistakes
Mistake 1: Assuming setTimeout always returns a number
Broken code:
let id: number = setTimeout(() => {
console.log("hello");
}, 500);
Why it fails:
- In browser-only typing, this is fine.
- If Node typings are included, the return type may not be
number.
Better:
let id: ReturnType<typeof setTimeout> = setTimeout(() => {
console.log("hello");
}, 500);
Mistake 2: Forcing the type with a cast
Broken or risky code:
let id = setTimeout(() => {}, 500) as number;
Comparisons
| Approach | Best for | Return type | Pros | Cons |
|---|---|---|---|---|
setTimeout(...) | General use | Depends on visible typings | Short and familiar | Can be ambiguous when both DOM and Node types exist |
window.setTimeout(...) | Browser-only code | number | Explicit, clear, fixes browser typing | Only works in browser environments |
global.setTimeout(...) / Node global | Node-only code | Node timeout object | Explicit for server code | Not for browser code |
ReturnType<typeof setTimeout> | Shared or mixed environments |
Cheat Sheet
// Browser-only
let id: number = window.setTimeout(() => {}, 500);
// Portable across browser/Node typings
let timer: ReturnType<typeof setTimeout> = setTimeout(() => {}, 500);
clearTimeout(timer);
Key rules
setTimeoutreturn type depends on the environment.- In browsers, it is usually
number. - In Node.js, it is a timeout object.
- If Node typings are included,
setTimeoutmay no longer type-check as returningnumber.
Best choices
- Use
window.setTimeout(...)for browser-only code. - Use
ReturnType<typeof setTimeout>for shared or mixed code. - Avoid
as numberunless you fully understand the tradeoff.
Useful tsconfig.json settings
FAQ
Why does TypeScript think setTimeout returns a Node type?
Because Node typings are visible in your project, often through @types/node or a dependency that includes them.
How do I force the browser version of setTimeout?
Use window.setTimeout(...) in browser code. This makes the DOM version explicit.
What is the safest type for a timeout handle in TypeScript?
ReturnType<typeof setTimeout> is usually the safest choice because it adapts to the active environment.
Should I type timeout IDs as number?
Only if your code is definitely browser-only and you are intentionally using the DOM version, often as window.setTimeout(...).
Can I remove Node typings from a frontend project?
Yes, often by adjusting compilerOptions.types in tsconfig.json, or by reviewing dependencies that bring in @types/node.
Does clearTimeout also have environment-specific typing?
Yes. That is why using the matching timeout handle type is important.
Is as number a good fix?
Mini Project
Description
Build a small search-input debounce utility in TypeScript. This project demonstrates the real reason timer handle types matter: you often store a timeout, cancel the previous one, and create a new one when the user keeps typing. This pattern is common in frontend apps and utility libraries.
Goal
Create a debounced function that delays work until the user stops triggering events for a short time.
Requirements
- Create a function that accepts a callback and a delay in milliseconds.
- Store the timeout handle so the previous timeout can be cancelled.
- Use a timeout handle type that works correctly with TypeScript.
- Call the callback only after the delay has passed without a new trigger.
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.
Angular @ViewChild Returning Undefined: Lifecycle, Child Components, and Fixes
Learn why Angular @ViewChild can be undefined, when it becomes available, and how to access child components correctly using lifecycle hooks.