Making setTimeout return number instead of NodeJS.Timeout in TypeScript
For the longest time we had to deal with the fact that NodeJS types were being included in a web project at work. The most visible aspect of this issue was that the return type of setTimeout
was a NodeJS.Timeout
when it should be a number
. In order to fix this we used to place the following at the top of each file that used setTimeout
:1
declare let setTimeout: WindowOrWorkerGlobalScope['setTimeout'];
We thought these NodeJS types were part of TypeScript just like the DOM types are part of it so we simply worked around them.
One day, while perusing TypeScript’s compiler options,
we discovered explainFiles
which explains in detail how each typing gets included in a project. Using it in our project yielded this:
$ tsc --explainFiles
node_modules/@types/node/index.d.ts
Type library referenced via 'node' from file 'node_modules/@types/graceful-fs/index.d.ts' with packageId '@types/node/index.d.ts@20.5.7'
node_modules/@types/node/index.d.ts
Type library referenced via 'node' from file 'node_modules/@types/cacheable-request/index.d.ts' with packageId '@types/node/index.d.ts@18.11.17'
So, there were at least two type dependencies that were causing the dependency @types/node
to be included in the project. But why were the types of graceful-fs
and cacheable-request
being included in the first place?
Diving again into TypeScript’s compiler options gave us the answer, in the form of another compiler option – types
:
By default all visible
@types
packages are included in your compilation. Packages innode_modules/@types
of any enclosing folder are considered visible.If
types
is specified, only packages listed will be included in the global scope.
Our project was unnecessarily including the types of dozens of development dependencies! Setting the compiler option types
to an empty array prevented all node_modules/@types
from being automatically included. And just like that, the return type of setTimeout
now finally appeared as number
.
We should observe that while using types
is the solution to avoid NodeJS types in a web project, this compiler option can cause the project’s tests to fail if they do depend on NodeJS types. In that case, we need a way to include those test-specific types only when running the tests.
In our project, we had to do the following changes:
- Create a
tsconfig.test.json
file that extends the maintsconfig.json
file. - Configure
jest
to use this new file. - Add
types: ["jest"]
to include the type definitions of@types/jest
(likedescribe
,it
,beforeEach
, etc). - Avoid adding
"node"
to thetypes
array. While it would be somewhat accurate to do this, the issue is that by doing that we’d “pollute” the global typing environment during tests. All thesetTimeout
s would have a return type ofnumber
during ourbuild
command, but would have a return type ofNodeJS.Timeout
during ourtest
command. So ourbuild
command would work but ourtest
command would fail. - Rename
global
toglobalThis
in our tests sinceglobal
is a NodeJS concept. -
Use Triple-Slash Directives in tests where
node
types are really necessary (for example, when usingfs
orpath
):/// <reference types="node" />
While our project is a web project, we were still using some NodeJS concepts to build the project, mostly in this form:
if (process.env.NODE_ENV !== 'production') {
console.error('Log something only necessary in development');
}
This code no longer compiled without the NodeJS process
type. Adding a new declaration file with our own type solved this:
// globals.d.ts
declare let process: {
env: {
NODE_ENV: string;
}
}
This actually took a little more work than the simple workarounds we were using before but it was worth it. A notable outcome of these changes was that our build
and test
command times improved by about 1 second. It’s possible that VSCode’s auto-completion and auto-import features also improved. And honestly, we’d rather our project only include the exact types that we want instead of grabbing everything from node_modules/@types
.
-
Another option was to surrender to
NodeJS.Timeout
and useReturnType<typeof setTimeout>
instead ofnumber
everywhere. ↩