iTranslated by AI
Deep Dive: How to Define True Global Variables in TypeScript

What You Thought Were Global Variables in TypeScript/JavaScript Were Actually File-Scoped Variables
In JavaScript and TypeScript, there is a common feeling that declaring a variable at the top level makes it a global variable, but strictly speaking, it becomes a file-scoped (or module-scoped) variable. Although it is file-scoped, it can be referenced from anywhere by using import, so it can effectively be treated as a global variable. Note that for ES modules in JavaScript or TypeScript, you use import, while for CommonJS, you use require.
ES modules in TypeScript:
import gDefaultSize from './lib';
console.log(gDefaultSize);
CommonJS in JavaScript:
const { gDefaultSize } = require('./lib.js');
console.log(gDefaultSize);
You cannot overwrite or reassign a value to an imported variable. This is by design.
However, if it is a true global variable, you can overwrite its value.
So, how should you write global variables? Those who are well-versed might have heard of it, but in short, you can use the globalThis object. However, if you think you "understand everything" just with that, you might run into various pitfalls. This article explains several patterns for writing global variables in JavaScript and TypeScript. It also covers how to resolve dependencies for initial values and addresses specific issues when using Jest.
globalThis.gDefaultSize = 100; // Knowing only this is insufficient, and in practice, it shouldn't be written this way
Are Global Variables an Anti-pattern?
Some people ruin things with the simple mindset that you should never use global variables. However, things that are effectively global variables—accessible from anywhere—do exist, such as React's Context or Redux. Since file-scoped variables can also be referenced anywhere using import, they can also be viewed as global variables.
However, there are points to be cautious about. A common concern is that it might become unclear where a value was changed. But countermeasures for that problem already exist. Furthermore, even with global variables, you can prevent someone from changing them arbitrarily. The problem lies elsewhere. For details on precautions and solutions for global variables, refer to the second half of this article. Global variables aren't all bad; by using them in the right places, you can grasp the overall picture of your application's data structure.
Project for Verification
The code written in this article can be verified with the project found here. At the time of writing, I have confirmed the operation with Node.js 20.19.0 installed.
Definition, Declaration, and Initialization of Global Variables
To define a global variable (declaring and initializing it), write it as follows:
TypeScript syntax:
declare global {export const __Variable__: __Type__} (globalThis as any).
__Variable__ = __InitialValue__;
TypeScript Sample 1:
declare global {export const gDefaultClientName: string} (globalThis as any).
gDefaultClientName = "user1";
TypeScript Sample 2: (1b.global.ts)
declare global {export const gDefaultSize: number} (globalThis as any).
gDefaultSize = 100;
- I strongly recommend writing the first half of the definition in a single line. It is a standard idiom, and since it says
declare global const, it is reasonably readable. This is explained in detail in the "Structure of Global Variables" section. - I strongly recommend starting variable names with
gto make it immediately obvious that they are not subject to the usualimportrules.
- By using the file name (module name) or a name unique to it as the variable name, collisions with other files (modules) will almost never occur.
- When it's time to turn it into a library, you change the global variables to file-scoped variables. Applications don't need to think deeply about collisions. If a collision occurs, it can be easily fixed.
When declare global Causes an Error
If there is not a single export other than declare global, an error will occur at global in declare global.
To avoid the error, also write export {}.
declare global {export const gDefaultTimeOut: number} (globalThis as any).
gDefaultTimeOut = 500;
export {} // Necessary when there are no other exports
Referencing Global Variable Values and Definitions
To get the value of a global variable defined in another file, you almost never need to import the variable name beforehand. However, in rare cases, you may need to import the file itself. If you get an error saying the global variable name cannot be found, write an import for the file. Especially when running Jest, since only some source files are loaded, you might get a "name not found" error only when executing Jest.
main.ts:
import './lib' // Write only when the global variable is not found
function main() {
console.log(gDefaultLength);
}
lib.ts:
declare global {export const gDefaultLength: number} (globalThis as any).
gDefaultLength = 100;
In the sample below, only OtherClass is imported, but at the same time, the gDefaultLength global variable also becomes available. Generally, since it is rare to refer only to a global variable, you almost never write code that imports only the file where the global variable is defined.
main.ts: (7a.importGlobal.ts)
import { OtherClass } from './lib'; // The global variable also becomes available
function main() {
console.log(gDefaultLength);
const object = new OtherClass();
}
lib.ts: (7b.importGlobal.ts)
declare global {export const gDefaultLength: number} (globalThis as any).
gDefaultLength = 100;
export interface OtherClass {
____
}
You don't even need to directly import the file where the global variable is defined. In the case above, the import is main.ts → lib.ts, but if it is main.ts → sub.ts → lib.ts, you only need to write the import for sub.ts in main.ts. For this reason, the need to write such imports arises much less frequently compared to general imports.
main.ts: (7a.importGlobal.ts)
import './sub'
function main() {
console.log(gDefaultTimeOut);
}
Jumping to Definitions
Using VSCode or similar editors, you can jump to variable definitions. By clicking a global variable name while holding the Ctrl key, you can jump to its definition. Furthermore, you can jump from the type written in the definition to the definition of that type itself.
main.ts:
console.log(gDefaultClientName);
When clicking gDefaultClientName while holding the Ctrl key,
lib.ts:
declare global {export const gDefaultClientName: User} (globalThis as any).
gDefaultClientName = {
name: "",
id: 0,
};
is displayed, and if you further click User while holding the Ctrl key,
lib.ts:
export interface User {
____
}
is displayed.
When searching for global variable definitions using the grep command, use a command like the following:
grep -rn "declare.*__Variable__" "__Project__/src"
Methods for Assigning Values to Global Variables
To overwrite or reassign a value to a global variable, you should call a method (setter method) such as Set__Module__.__Variable__(__Value__).
main.ts: Syntax
import { Set__Module__ } from '__Path__';
function main() {
Set__Module__.__Variable__(__Value__); // Assign a value to the global variable via the method
__Variable__ = __Value__; // Assignment is an error because it's const
}
lib.ts: Syntax
export class Set__Module__ {
static __Variable__(x: __Type__) {
(globalThis as any).__Variable__ = x; // When debugging, write code here to call console.log or set a breakpoint
}
}
Example:
main.ts: (1a.main.ts)
import { SetBuffer } from './buffer';
function main() {
SetBuffer.gDefaultSize(120); // Assign a value to the global variable via the method
gDefaultSize = 120; // Assignment is an error because it's const
}
buffer.ts: (1b.global.ts)
export class SetBuffer {
static gDefaultSize(x: number) {
(globalThis as any).gDefaultSize = x; // When debugging, write code here to call console.log or set a breakpoint
}
static gDefaultClientName(x: string) { // Method to assign to another global variable
(globalThis as any).gDefaultClientName = x;
}
}
The __Variable__ in Set__Module__.__Variable__ is actually the method name. Make the method name exactly the same as the global variable name. By doing so, the code for assignments will also be found during a full-text search. Generally, a method named set__Variable__ is often introduced, but that leads to the problem where assignment code no longer hits in search results. I recommend using this format even when assigning to file-scoped variables.
By ensuring that a method is always called when overwriting or reassigning, you can reliably track changes in values by writing code to log with console.log or by setting a breakpoint within that method. Defining global variables with const ensures that the assignment method must be used (unless you perform the "cheat" of changing it to an any type). That said, since similar steps are required to find value changes in object properties as well, it's not a matter of global variables being inherently bad.
Listing and Jumping to Assignment Code
In VSCode, if you Ctrl+click the method name written in the method definition, you can list all locations where the assignment code is located. You can also jump to those locations. Since code that only references the global variable's value is not listed, there is no search noise.
Resolving Dependencies of Initial Values
The biggest pitfall in handling global variables is the dependency of initial values. If you write initial values as literals, there are no dependencies. However, to ensure centralization, if you write global constants defined elsewhere as initial values, dependencies arise, and appropriate measures are required.
Case of Literals
If you write the initial value as a literal, there are no dependencies, so there is nothing special to worry about.
declare global {export const gDefaultUser: string} (globalThis as any).
gDefaultUser = "user1";
In the case of an object's initial value, if you write literal initial values inside an object literal, there are no dependencies, so there is nothing special to worry about.
declare global {export const gTarget: Target} (globalThis as any).
gTarget = {
name: "user1",
num: 111,
};
export interface Target {
____
}
However, writing as literals might lead to issues with centralizing specific values. For example, by setting the initial value of gTarget.name above to gDefaultUser, you can centralize the description of the default username "user1" within the definition of gDefaultUser.
In the Case of the Same File
In the following case, the initial value of gDefaultClientName depends on gDefaultUser. When writing the dependent definition in the same file, the definition of the dependency, gDefaultUser, must be written above it.
declare global {export const gDefaultUser: string} (globalThis as any).
gDefaultUser = "user0";
declare global {export const gDefaultClientName: string} (globalThis as any).
gDefaultClientName = gDefaultUser; // The definition of gDefaultUser must be written above this line
Writing them in reverse order will result in an error.
declare global {export const gDefaultClientName: string} (globalThis as any).
gDefaultClientName = gDefaultUser; // ReferenceError: gDefaultUser is not defined
declare global {export const gDefaultUser: string} (globalThis as any).
gDefaultUser = "user1";
In the Case of a Different File
If the variable used as the initial value is defined in another file, you need to use an import statement to reference that variable. As mentioned earlier, this is the same as writing an import statement to make a global variable available.
Note that only global variables or file-scoped constants can be referenced for initial values (other than literals).
When There is a Circular Dependency with Another File via import
If the variable used as the initial value (a global variable, or a file-scoped constant or variable) is defined in another file, you need to use an import statement. If a circular dependency (circular reference, mutual dependency) occurs in that import, you will need to edit it to separate the shared parts. If A imports from B and B imports from A, you resolve the circular dependency by moving the shared execution parts, such as value assignments, to C, and having both A and B import from C. However, the global variable definitions explained so far can be divided into:
- Declaration and initialization
- Assignment methods
Among these, assignment methods are defined during the pre-execution analysis phase, so they are not candidates for separation as shared parts. On the other hand, initialization is performed at runtime, so if the imports are circularly dependent, it can become a target for separation into a shared part.
I will explain this concretely below. First, here is the problematic code.
Before fix a.ts: Definition of __VariableA__
import { __IdentifierB__ } from 'b';
declare global {export const __VariableA__: __Type__} (globalThis as any).
__VariableA__ = __InitialValueA__;
Before fix b.ts: Definition of __VariableB__
import { __IdentifierA__ } from 'a';
declare global {export const __VariableB__: __Type__} (globalThis as any).
__VariableB__ = __VariableA__;
In reality, the initialization itself is not a circular dependency. Since the dependency is __VariableB__ → __VariableA__ → __InitialValueA__, it is not circular. What is circular is the files that are importing each other. The dependency a.ts → b.ts → a.ts is circular. Errors involving circular dependencies in imports occur when files are circular at the file level (broad level), even if there is no circular dependency at the language level (fine level) such as between variables or functions, and this is what needs to be fixed.
If there is a circular dependency at the language level, fix that first. The following is the solution when there is only a file-level circular dependency.
You can determine what the shared part is based on the identifiers used in both a.ts and b.ts. In the sample above, it is __VariableA__, which is defined by a.ts and referenced by b.ts. Therefore, you separate the definition of __VariableA__ into a new, separate file. Since the initial value should be a constant, I think it's good to include Const as part of the new file's name. Incidentally, the term "circular dependency" describes a relationship, so it feels out of place to include it in a file name. A file name doesn't represent the relationship between files.
Below is the code that solves the problem.
After fix a.ts: (8a.circularDependency.ts)
import { __IdentifierB__ } from 'b';
import 'b'; // Depending on the type of __IdentifierB__, if the above import behaves as an import type, you need this to initialize global variables
After fix aConst.ts: (8aConst.circularDependency.ts)
import type { __Type__ } from 'a'; // If you need to reference a "type" defined during analysis, use import type. It is not a regular import.
export {} // Necessary if there are no exports
declare global {export const __VariableA__: __Type__} (globalThis as any).
__VariableA__ = __InitialValueA__;
// The content is the same as a normal global variable declaration and initial value code
After fix b.ts: (8b.circularDependency.ts)
import type { __IdentifierA__ } from 'a';
import 'aConst'; // Import the shared part
declare global {export const __VariableB__: __Type__} (globalThis as any).
__VariableB__ = __VariableA__;
If it doesn't work, handle it by writing console.log before and after the import statements to check the behavior.
Reference: Structure of Global Variables
If you write the declaration of a global variable in a style that emphasizes the structural syntax of TypeScript, it looks like this:
TypeScript:
declare global {
export const __Variable__: __Type__;
}
(globalThis as any).__Variable__ = __InitialValue__;
This style conforms to the structure of global variable declaration statements in TypeScript. While this format might seem clearer while you are trying to understand the structure, I strongly recommend the style of writing the first half of the definition on a single line so that unnecessary information (like "what was globalThis as any again?") doesn't clutter your view during daily use.
Description:
-
declare globalis the keyword indicating the start of a global variable declaration. - You can have as many
declare globalblocks as you want in a single file. In other words, there is no need to gather all the global variables in a file into a singledeclare globalblock. -
exportis the keyword indicating that it can be accessed from anywhere. -
constis a modifier indicating that the value of the global variable can be referenced as a constant. While you can declare it withvar, we useconstto avoid the problem of not knowing where a value was changed. However, by using the special syntax shown immediately below, you can bypass theconstrestriction to assign a value to the global variable. -
__Variable__is the variable name. Replace this with your actual variable name. This description allows access viaglobalThis.__Variable__. -
__Type__is the type of the variable. Replace this with the actual type name. Since the type cannot be inferred from the initial value, you must write it explicitly. -
globalThisis the object containing all global variables. -
(globalThis as any)isglobalThiswithout static type checking. By performing a type assertion (type cast) to theanytype, you can bypassconstand assign a value to the global variable. However, this syntax should be restricted to initialization and inside assignment methods (explained later). -
__InitialValue__is the initial value of the global variable. If you don't write this, the initial value will beundefined, which forces the type to be a null/undefined-nullable type. Since that is difficult to handle, avoid usingundefinedas an initial value. Assign a value that is acceptable even if it's never changed from its initial state.
Are Global Variables an Anti-pattern?
Global variables require careful design, or they can become difficult to handle. Here are some points to note. While the following explains global variables, the same caution is required for file-scoped variables in JavaScript/TypeScript. Similar care is needed for get~ and set~ functions that access globally shared data, functions that access files or databases, and environment variables.
The problem with global variables is not that they can be referenced in the global scope. The main issue is sharing value changes globally.
The issue of not knowing where a change occurred has already been resolved, as shown earlier, by using the assignment method Set__Module__.__Variable__. However, since object properties also suffer from the problem of not knowing where they were changed, the idea that global variables are inherently bad is a prejudice.
It is not always better to write code that avoids global variables from the start. For simple systems, global variables are easier to handle. Many middlewares have system configuration variables that can be set via corresponding environment variables (which are effectively global variables). However, there are certainly cases where global variables should not be used.
Making "current" or "active" items global variables is poor design. On the other hand, making "defaults" global variables is perfectly fine. Defaults are things where you don't set a value if you don't care about it. Of course, if something intended as a default doesn't work correctly, you may need to refactor to avoid using global variables. In any case, avoid blindly declaring that all global variables are forbidden and instructing countermeasures before they are needed; be careful not to fall into over-engineering.
Good global variables:
- Constants and immutable data
- Defaults
- Collections of all active items
Bad global variables:
- Current items
- Active items
- Contexts
Rather than adding arguments to many methods, then to the methods they call, and so on—destroying interfaces along the way—making data accessible from the global scope allows you to understand the data structure as a tree, making the entire program easier to grasp.
The Problem of Requiring Restoration Logic
Data that can be referenced in the global scope is convenient and clear because it can be accessed from anywhere, but its content should generally not be changed. A typical example of content that changes frequently is a global variable pointing to what is "current" or "active." Switching the active item to perform a specific process is a common pattern, but if the active item is stored in a global variable, there is a very high probability that logic to restore the value to its original active state after the process finishes will be required. This is a major issue with global variables. Upon realizing this, you might think that all global variables require restoration logic, but in reality, it's only those pointing to current or active items. Default values do not need to be restored.
declare global {export const gCurrentPower: number} (globalThis as any).
gCurrentPower = 1;
function main() {
liftUp(); // gCurrentPower == 1
sub();
liftUp(); // gCurrentPower == 1
}
function sub() {
const oldPower = gCurrentPower;
gCurrentPower = 2;
liftUp(); // gCurrentPower == 2
gCurrentPower = oldPower; // As shown here, restoration logic is required
}
function liftUp() {
console.log(`liftUp: power=${gCurrentPower}`);
}
When such processing becomes necessary, you can judge that a refactoring to stop using global variables is required.
You might think you can just set the value every time without this restoration logic, but then there's no point in making it a global variable. On the other hand, continuing to use it as a global variable isn't good either. Ultimately, you will need to refactor to stop using global variables.
Designing Immutable Data (Immutable Data Modeling)
The problem with global variables is not that they can be referenced in the global scope. The main issue is sharing value changes globally. Therefore, sharing constants globally is not a problem.
export const bufferSize01 = 100; // OK
export const bufferSize02 = 102; // OK
export const bufferSize03 = 105; // OK
Options for starting a program also become immutable data while the program is running, so making them global variables is not a problem.
function main() {
parseProgramOptions();
console.log(`gProgramOptions size: ${gProgramOptions.size}, client: ${gProgramOptions.client}`);
}
declare global {export const gProgramOptions: ProgramOptions} (globalThis as any).
gProgramOptions = {};
export function parseProgramOptions() {
commander.program
.version('0.1.0')
.option("-s, --size <i>")
.option("-c, --client <s>", "Client name. --client system")
.parse(process.argv);
(globalThis as any).gProgramOptions = commander.program.opts<ProgramOptions>();
}
When there are global variables whose values change, a common way to redesign variables into constants is to turn them into time-series data. For example, when placing an Excel file on a file server, you might include the date and time in the file name and make it a rule not to change files that already have a timestamp (treating modification as a violation of the rule). If someone else is editing, there is a possibility of seeing a file in an inconsistent, halfway state, but by including the date/time element in the name (ID, number, etc.), the problem of seeing an incomplete state disappears.
By designing to handle immutable data separated by time series instead of handling changing data—that is, by using immutable data modeling (design)—data can be shared safely.
Broadly speaking, the benefits of immutable data modeling are as follows:
- State management (e.g., in Redux, creating a new State instead of directly modifying it)
- Concurrent processing (avoiding race conditions)
- Time travel debugging (reproducible by holding past states)
- History tracking (recording transitions of states)
The disadvantages are:
- Increased memory usage and decreased processing speed due to creating copies
- Design ingenuity required when using foreign keys
Placing in a Context Object
If the value does not change during a certain set of processes, instead of setting it every time, store the value in a context object (context).
This is a common workaround suggested by those who prohibit global variables: moving what was a global variable into a property of a context object.
When defining a context object: (3.context.ts)
export function main() {
const service1 = new Service({power: 110, time: 40, activeUser: user1});
service1.process();
}
class Service {
context: Context;
constructor(context: Context) {
this.context = {... context}; // [... ] is shallow copy.
}
process() {
console.log(`Processing data for ${this.context.activeUser.name}: power: ${this.context.power}, time: ${this.context.time} ...`);
}
}
interface Context {
power: number;
time: number;
activeUser: User;
}
When setting properties directly on an object: (4.objectWithContext.ts)
export function main() {
const service1 = new Service({power: 110, time: 40, activeUser: user1});
service1.process();
}
class Service {
power: number; // Context value 1
time: number; // Context value 2
activeUser: User; // Context value 3
constructor(parameters: Parameters = gDefaultParameters) {
this.power = parameters.power || gDefaultPower;
this.time = parameters.time || gDefaultTime;
this.activeUser = parameters.activeUser || gDefaultActiveUser;
}
process() {
console.log(`Processing data for ${this.activeUser.name}: power: ${this.power}, time: ${this.time} ...`);
}
}
interface Parameters {
power: number;
time: number;
activeUser: User;
}
However, moving to context object properties doesn't necessarily mean global variables will disappear. For frequently used values, it is more convenient to use default values accessible from the global scope without specifying parameters. On the other hand, exceptional values might be specified locally. A clear example of this is timeout values in an E2E test suite. Ultimately, a specification that includes **all three—global, context, and local—**is adopted by many middlewares.
Aggregates of All Instances Can be Placed Globally
The problem with global variables is not accessibility from the global scope. Since files and databases are accessible from anywhere in a program, the data itself is in the global scope, but if you keep the pointers to the data, such as filenames or record IDs, local, then "restoration logic" will not be necessary. With this structure, there is no need to worry about someone arbitrarily changing data just because it's placed globally.
If you prohibit all global variables, you won't be able to place any data globally and will be forced to place everything locally, which is extremely inconvenient.
Before refactoring: (5-NG.current.ts)
const gCurrentLifter = {power: 100, time: 333};
liftUp();
gCurrentLifter.power = 110;
gCurrentLifter.time = 444;
liftUp();
After refactoring: (6.allInstances.ts)
export function main() {
liftUp(); // Use default value
liftUp(0); // As long as the index (0) isn't global, gCurrentLifters can be global
liftUp(1); // Parallel operation is also possible
}
const gCurrentLifters = [
{power: 110, time: 444}, // gCurrentLifters[0]
{power: 140, time: 666}, // gCurrentLifters[1]
{power: 100, time: 333}, // gCurrentLifters[2]
];
function liftUp(index: number = defaultLifterIndex) {
const lifter = gCurrentLifters[index];
console.log(`liftUp power: ${lifter.power}, time: ${lifter.time}`);
}
const defaultLifterIndex = 2;
Summary
As explained above, global variables are not inherently bad. By using them in the right places, you can grasp the overall picture of your application's data structure not only through variables within the main function but also through global variables.
Discussion