About NodeJS globals

2022-02-14 - Updated for @types/node@16

NodeJS allows you to set process-level global values that can be accessed in any module.

In general I would consider implicitly sharing global values between modules a bad practice, however in certain situations it can be a pragmatic choice.

Assigning globals using pure JavaScript is straightforward, making them work with TypeScript is another thing. Let’s dive in.

NodeJS globals in JavaScript

Setting a global value in NodeJS couldn’t be simpler:

1
2
3
4
5
6
7
8
global.someValue = 'My hovercraft is full of eels';

// In another module:

console.log(global.someValue);

// Omitting `global` works too:
console.log(someValue);

NodeJS globals in TypeScript

Note: Make sure that you have @types/node installed.

Let’s start by trying to just assign the value and see what happens:

1
2
global.someValue = 'My hovercraft is full of eels';
// TypeError: Property 'someValue' does not exist on type 'Global'.

If we look at the type of global it’s globalThis. Fortunately TypeScripts declaration merging allows us to extend this interface to include our new attribute:

1
2
3
4
5
6
7
8
9
10
11
declare global {
var someValue: string;
}

// Assignement works fine now
global.someValue = 'My hovercraft is full of eels';

// It also can be accessed everywhere now:
console.log(global.someValue); // OK!

console.log(someValue); // OK!

This declaration can be placed in any .ts file in your project. Just make sure that it is imported and the global value is assigned before any other module would use it.

Note: For @types/node < 16 you need to go with:

1
2
3
4
5
6
7
8
declare global {
var someValue: string;
namespace NodeJS {
interface Global {
someValue: string;
}
}
}

Actual real life use cases

Making winston logger globally available:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// logging.ts

import * as winston from 'winston';

declare global {
type Log = winston.Logger;
var log: Log;
}

export const setDefaultLogger = (logger: winston.Logger) =>
(global.log = logger);

// index.ts

import * as winston from 'winston';
import * as logging from './logging';

logging.setDefaultLogger(winston.createLogger(...));

// someModule.ts:

// Note - no need to import 'log'!

const doImportantStuff = () => {
log.info('Doing important stuff!');
}

Making ramda globally accessible:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import * as ramda from 'ramda';

declare global {
var R: typeof ramda;
}

global.R = ramda;

// someModule.ts:

// Note - no imports!

const doImportantStuff = () => {
const x = R.sortBy(...);
}

Final thoughts

You may wonder if it makes sense to use an implicit global instead of just importing the value explicitly.

I don’t think there is one answer to that. For example, in my case, using the implicit log from example above felt smoother than having to import it every time when I wanted to log something, even with IDE automated imports. I feel that this made me use log more often which led to slightly better quality of application logs. It can also remove the headache of constantly adding and removing the import statement if it turns out that the last log was removed and linter throws errors at you that it is no longer needed. Also having the globals explicitly typed, with clear lifecycle doesn’t make them look like a hack.

Just keep in mind that abusing global can lead to a codebase that is both hard to read and reason about.