How to Declare Global Variables in Typescript

Share

In Typescript, there are multiple ways to declare global variables and not all of them work depending on what you're doing. In this article, I'll explain what are they.

Foreword: VS Code Trouble

If you're using VS Code you may run into issues EVEN if you do everything right. I'll explain at the end of the article how to solve them.

1: Var

The simplest method is to just use var at the root scope if you don't have any export or import statements when you're defining the variable (i.e. it's YOUR variable, and doesn't come from a third party library or from the environment).

var my_global_namespace = {
    foo: 123
};

To Typescript, this will be a global variable and will be made available to every file.

2: Declare Var

If you don't define the global variable yourself but it comes from a third-party library (e.g. JQuery), or you conditionally define it using window, you can declare that it will exist using the declare keyword.

Like var, this only works if you don't have any export or import statements. Unlike var, this doesn't generate Javascript code, as it's a declaration purely for Typescript use.

type MyGlobals = { foo: number };
declare var my_global_namespace: MyGlobals;

If there is the possibility that some of your code will refer to the global variable before it's defined, it's a good idea to mark it as potentially undefined so Typescript will issue an error if you try to use it without checking if it's undefined first (this only happens if you have strictNullChecks enabled).

declare var my_global_namespace: MyGlobals | undefined;

3: Declare Global { var }

If you have import or export statements, the code above won't work.

import { getFoo } from "FooGetter";

// no longer global
var my_global_namespace = {
    foo: getFoo()
};

That's because when you do this, the .ts file is considered to be a module, which means that var statements won't become global.

More technically, even if you use a bundler, for example, the module code will be wrapped in a function that is called immediately. This is called Immediately Invoked Function Expression (IIFE). When you have a var outside of a function, the variable is defined in the global scope and becomes available in window in a web browser. If you have var inside of a function, it becomes scoped inside the function, so you can't access it globally.

(function() {
    var my_local_var = 123;
})();

This is why var no longer makes the variable global.

We can make it global inside a function by setting it in window directly in Javascript.

(function() {
    window.my_global_var = 123;
})();

console.assert(my_global_var === 123);

However, if we try to do this in Typescript, we'll get an error that says "Property 'my_global_var' does not exist on type 'Window & typeof globalThis'," because we tried to set window.my_global_var, and window doesn't have a my_global_var property by default.

We can fix this inside a module using declare global.

import { getFoo } from "FooGetter";

type MyGlobals = { foo: number };

declare global {
    var my_global_namespace: MyGlobals;
}

window.my_global_namespace = {
    foo: getFoo()
};

The purpose of declare global is just to "get out" of the module scope and into global scope before making declarations. We don't need to use declare var inside declare global.

Troubleshooting

Making Globals Available Without Importing a File

If you place globals in a .ts file, they will only become available if the file is included in your project. This means you can't have the globals without compiling that specific file. If the globals come from a third-party library, it's a good idea to place the file in a separate file, typically called globals.d.ts, that never gets imported by anything.

Typescript Generates a global.d.js File

This happens if you explicitly import globals.d.ts. Don't do that.

// Don't 
import "globals.d.ts"

globals.d.ts is Ignored by Typescript

This happens if globals.d.ts isn't included in the file list compiled by tsc. If you use a tsconfig.json file, make sure to include globals.d.ts in your file list.

Typically this isn't necessary because most people just use a glob pattern like this in their tsconfig.json:

{
    "include": ["src/**/*.ts"]
}

Since src/**/*.ts matches src/globals.d.ts, globals.d.ts will be included.

However, if globals.d.ts doesn't contain any statements, only declarations, a Javascript file (globals.d.js) won't be generated.

// This is a declaration. It's okay.
declare var foo: number

// Also okay.
type BigNumber = number;

// This is a statement. Not okay.
var bar: number;

// This is also a statement. Not okay.
var baz = 1;

In fact, I'm pretty sure you get an error if you do this.

If you use var bar: number;, the error is "Top-level declarations in .d.ts files must start with either a 'declare' or 'export' modifier."

If you use var baz = 1, the error is "Initializers are not allowed in ambient contexts."

I'm not sure, but I remember having issues with declaring types in Typescript in d.ts files when the declaration ended in a semi-colon (;).

// Worked for some reason
type Type1 = { /* ... */ }

// But this didn't?
type Type2 = { /* ... */ };

VS Code Shows Typescript Errors when Typescript Compiler Doesn't

One complex issue you can have in VS Code is that things will look like they're working when you have the globals.d.ts open in a tab, but when you close the tab, you'll start getting errors; and, additionally, the errors you're getting from VS Code don't even appear when you compile Typescript with tsc. This is specially confusing when you're running Typescript in watch mode. Then your code always compiles with zero errors, but VS Code keeps telling you that your code is all wrong.

There are two reasons for this issue. First, and primarily, it happens because you don't have a tsconfig.json (or jsconfig.json for Javascript projects), so the language server considers only the open documents as being part of the project.

This is the expected behavior; if you don't have a jsconfig, only opened file are considered part of your project

globals.d.ts only recognized when open without jsconfig or tsconfig file [https://github.com/microsoft/vscode/issues/94333] (accessed 2024-11-24)

Second, if you DO have a tsconfig.json, the problem becomes that VS Code is ignoring your tsconfig.json.

VS Code only reads a tsconfig.json if it's at the root of your project. This means if you have it saved as src/tsconfig.json, or you have multiple tsconfig.json files, like tsconfig.development.json and tsconfig.production.json, VS Code is going to ignore them.

In this case, one solution is to create a tsconfig.json at the root of your project (typically where you have your package.json, etc.), and extend your actual configuration.

{
    /* This file is only here to make VS Code
    * stop ignoring my globals. */
    "extends": "./frontend/tsconfig.development.json"
}

Note: you can symlink it instead if they are in the same directory. If they are in different directories, the relative filepaths will resolve wrong and you'll get an error that your tsconfig.json doesn't include any files. For example, if ./frontend/tsconfig.development.json includes src/index.ts, the resulting filepath will be frontend/src/index.ts. This will work with extends. However, if instead of using extends you just symlink it to one directory above, then it will resolve to src/index.ts instead of frontend/src/index.ts, and there won't be a file in that filepath.

Additionally, you have multiple tsconfig.json in your project that compile different sets of files, you might want to make sure that VS Code's language server considers ALL files instead of only one subset of them. You can do this by overriding includes.

{
    /* This file is only here to make VS Code
     * stop ignoring my globals. */
    "extends": "./frontend/tsconfig.development.json",
    "include": ["frontend/src/**/*.ts", "backend/src/**/*.ts"]
}

This isn't the perfect solution since you could end up with missing types, e.g. if frontend has DOM in its library but not Node, and backend needs Node but not DOM, the only way to solve it would be to include BOTH of them in the main tsconfig.json.

Alternatively, you could try separating your backend and frontend in VS Code by using a multi-root project instead of placing everything into a single root. Then you would have one "folder" for the backend and one for the frontend, both open in a single multi-root workspace, and each "folder" can have its own tsconfig.json.

my-file.d.ts is Ignored by Typescript in VS Code

If you're a clever person, you may have figured that you can place all your types and declarations in a separate .d.ts file to keep your .ts file free of all that type of stuff, so you would have my-file.ts and my-file.d.ts. This works with the Typescript compiler, but doesn't work with VS Code's language server for some reason.

The problems seems to be that the language server thinks my-file.d.ts is just a simpler version of my-file.ts, so it ignores the .d.ts file if the .ts file is available.

You can fix this by adding more words to its filename, e.g. my-file.types.d.ts.

Written by Noel Santos.

About the Author

I'm a self-taught Brazilian programmer graduated in IT from a FATEC. In a world of increasingly complex and essential computers, I decided to use my technical expertise in hardware, desktop applications, and web technologies to create an informative resource to make PC's easier to understand.

View Comments