Globals & module augmentation
Tell TypeScript about globals from script tags with declare, add fields to Window, extend a package's types, and type your environment variables.
- 01Describe globals with declareUse
declare const,declare function, anddeclare globalfor names that a script tag or a build tool creates. - 02Add to Window and to packagesAdd an optional property to
Window, and add a field to another package's interface withdeclare module. - 03Type environment variablesDescribe your
import.meta.envandprocess.envvariables, and know what those types can't check.
A global TypeScript has never heard of
A shop page loads an analytics script with a <script> tag. The script makes a global: a name that every script on the page can use, here analytics. Your code calls it when a shopper adds tea to the cart:
analytics.track("add_to_cart");
console.log("Tea added to the cart");Line 1 calls analytics.track, which the analytics script wrote. It prints Tracked add_to_cart. Line 2 prints Tea added to the cart. In this lesson, a setup file stands in for the page's scripts, so every example runs the way the lesson says when you press Edit & run.
Now the same two lines in a TypeScript file:
analytics.track("add_to_cart");console.log("Tea added to the cart");TypeScript marks line 1 with Cannot find name 'analytics', on purpose here. It means "nothing in your code says what analytics is". TypeScript reads your files. It never sees the page's <script> tags, so a name that a script makes while the page runs is new to it. The code still runs and prints both lines, because the page does have analytics.
analytics while the page runs. TypeScript reads only your code, so it has never heard of the name and marks line 1.The fix is one line that describes the global. declare means "this exists already; here is its type":
declare const analytics: { track(event: string): void;}; analytics.track("add_to_cart");console.log("Tea added to the cart");Lines 1 to 3 tell TypeScript that a constant called analytics exists, with a track method that takes a string. They don't create anything. Line 5 now checks, and line 6 prints as before. There are no errors.
A new colleague calls, and your phone shows "Unknown caller". The colleague is real; your phone just hasn't been told about them. You save their name and number, and from then on the phone knows who is calling. Saving the contact didn't bring the colleague into the company. They were already there.
- In real life: A new colleague calls you
- In TypeScript: Your code calls
analytics, which a script made - In real life: Your phone shows "Unknown caller"
- In TypeScript: TypeScript says
Cannot find name 'analytics' - In real life: You save their name and number
- In TypeScript: You write
declare const analytics: { … } - In real life: Saving a contact doesn't hire anyone
- In TypeScript:
declaredoesn't create the global; the script still has to
TypeScript only knows the names your code declares. A global that a script tag, the server, or your build tool makes needs a declare line that describes it. declare global adds names that every file sees, and declare module "a-package" adds to a package's types. None of them create anything; they only describe what will be there when the page runs.
declare: describe a name, don't create it
You can declare any kind of name this way. A page often has a constant from the build tool and a function from a script:
declare const __APP_VERSION__: string;declare function gtag(command: "event", name: string): void; console.log(`Version ${__APP_VERSION__}`);gtag("event", "add_to_cart");- Line 1 declares
__APP_VERSION__, a string. Vite'sdefineoption writes the real version into your code when it builds, and Vite's guide tells you to declare it exactly like this. - Line 2 declares
gtag, the function Google's analytics script makes. A declared function has no body: only its parameters and what it gives back.voidmeans it gives back nothing useful. - Line 4 prints
Version 1.4.0, and line 5 printsgtag: event add_to_cart, from the stand-ins for the page.
A declare line is all type, so it's removed with the other types. This is what's left for the browser:
console.log(`Version ${__APP_VERSION__}`);
gtag("event", "add_to_cart");Lines 1 and 2 are the two lines that did something. Both declare lines are gone. TypeScript calls declared names ambient: they are described here and made somewhere else. That's why a declare line can't hold a value:
declare const shopName: string = "Tea House";Initializers are not allowed in ambient contexts means "a described name can't be given a value here". An initializer is the = "Tea House" part. If you want to make the constant, drop declare.
What you write after the colon decides how much TypeScript checks. Press each button and watch the list under the code. Line 4 has a typo on purpose: trak instead of track.
// analytics isn't declared analytics.track("add_to_cart");analytics.trak("checkout");2 errors: lines 3 and 4Prints Tracked add_to_cart, then line 4 throws a TypeErrorTypeScript has never heard of analytics, so it marks both lines: Cannot find name 'analytics'. The script on the page created it, but TypeScript only reads your code.
Only declare its shape catches the typo. With declare as any, TypeScript accepts everything, and line 4 throws a TypeError when it runs (the any, unknown & never lesson covers both types). The code runs the same way with every button, because line 1 is removed before it runs. A declaration changes what TypeScript says, never what the page does.
Globals from a module: declare global
Where you put a declaration matters. A file with an import or export is a module, and the names it declares belong to that file only (Modules & type imports covers this, and Why modules the JavaScript side). A file with neither is a script, and its top-level names are global: every file sees them. Most files in a project are modules, and so is every example on this page.
So declare const analytics in a module describes analytics for that one file. To describe it for every file, put it inside a declare global block:
type Analytics = { track(event: string): void }; declare global { const analytics: Analytics;} analytics.track("add_to_cart");Line 1 gives the analytics object's shape a name, Analytics, with a type alias. Lines 3 to 5 are the declare global block. Everything inside it is added to the global scope, the names every file shares. You don't write declare again inside it. Line 7 calls track and prints Tracked add_to_cart.
app.ts has an import, so it's a module. Its declare const tax is seen only by that file. declare global adds analytics to the global names, which every file sees, beside window and document.There are two places for global declarations, and you'll see both in real projects:
- A script file with no
importorexport, often namedglobals.d.ts. A.d.tsfile holds only types. Its declarations are global already, so you writedeclare const analytics: Analytics;without a wrapper. - A
declare globalblock in a module. You need this as soon as the file imports a type. Oneimportline turns a script into a module, and then its plaindeclare conststops being global: other files getCannot find nameagain.
A declare global block only works in a module. In a script file, TypeScript reports Augmentations for the global scope can only be directly nested in external modules or ambient module declarations: there, everything is global already. An augmentation is an addition to something that already exists.
Inside declare global, const and var aren't the same. Say the server writes the cart count into the page, and you read it as window.cartCount:
declare global { const cartCount: number;} console.log(cartCount);console.log(window.cartCount);Line 5 is fine: cartCount is a global name. Line 6 gets Property 'cartCount' does not exist on type 'Window & typeof globalThis'. That long type is what TypeScript gives window: everything the Window interface lists, joined with every global name declared with var or function. In JavaScript, a global var also becomes a property of window, but a global const or let doesn't (var and the global object shows why). TypeScript follows the same rule. The code still prints 3 twice, because the page set window.cartCount. Declare it with var instead:
declare global { var cartCount: number;} console.log(cartCount);console.log(window.cartCount);// ^?The only change is var on line 2. Line 6 now checks, and the query on line 7 shows that window.cartCount is the same global: var cartCount: number. Both lines print 3.
The table below sums it up. Read each row as: this declaration inside declare global, then the two ways to use it.
| Inside declare global | Can you write cartCount? | Can you write window.cartCount? |
|---|---|---|
const cartCount: number | Yes. | No. A const doesn't become a property of window. |
let cartCount: number | Yes. | No, for the same reason as const. |
var cartCount: number | Yes. | Yes. A global var is also a property of window and globalThis. |
interface Window { cartCount: number } | No. It adds a property to window, not a variable. | Yes. |
Adding to Window for a script that may not load
The Declaration merging lesson added a property to Window by declaring the interface again inside declare global. Your Window joins the browser's. Here is the question that lesson left open: what if the script that sets the property never runs?
Say the shop loads a help-chat script that sets window.chat. Ad blockers stop scripts like this, and a slow network can delay them. So the honest type is optional, written with a ?: the property may be missing.
type Chat = { open(): void }; declare global { interface Window { chat?: Chat; }} window.chat.open();console.log("Help requested");Line 1 names the chat's shape. Lines 3 to 7 add an optional chat to Window. Line 9 calls open without checking, and TypeScript says 'window.chat' is possibly 'undefined': the chat may not be there. On this page there is no chat script, so line 9 throws a TypeError and line 10 never runs.
type Chat = { open(): void }; declare global { interface Window { chat?: Chat; }} window.chat?.open();console.log("Help requested");Line 9 now uses ?., which calls open only when window.chat isn't undefined (Nullish and optional chaining covers it). The error is gone, and line 10 prints Help requested with or without the chat.
Notice that the code reads window.chat, not a bare chat. Reading a property that isn't there gives undefined. Reading a global name that doesn't exist throws, and ?. can't help:
chat?.open();
console.log("Help requested");Line 1 throws ReferenceError: chat is not defined before ?. gets a chance. So for anything a script might not create, add an optional property to Window and read it through window.
Adding to a package's types
Packages you install from npm come with their own types, in .d.ts files. Say the shop uses a small UI package called shop-ui. Its types describe a theme with a font size, and a createTheme function:
export interface Theme { fontSize: number;} export declare function createTheme(theme: Theme): Theme;Lines 1 to 3 describe Theme as an interface with a fontSize. Line 5 declares createTheme, which takes a Theme and gives one back. The shop wants a brand color in its theme too. You can't edit files in node_modules: the next install overwrites them. Instead, you add to the package's types from your own code with a declare module block. This is called module augmentation.
This playground checks a small project for real: your src/app.ts and the installed shop-ui. Press each button and watch the list under the code.
import { createTheme } from "shop-ui"; const theme = createTheme({ fontSize: 16, brandColor: "orange" });console.log(theme.brandColor);TypeScript checks this example.
2 errors: lines 3 and 4src/app.ts, shop-uiNoshop-ui's Theme only has fontSize. Line 3 gets Object literal may only specify known properties, and 'brandColor' does not exist in type 'Theme', and line 4 gets Property 'brandColor' does not exist on type 'Theme'.
With Nothing added, brandColor isn't part of Theme, so line 3 and line 4 are marked. With declare module "shop-ui", lines 3 to 7 add brandColor to shop-ui's Theme and every error is gone. The other two buttons show the two rules that break it most often.
Theme has fontSize. Your declare module "shop-ui" block adds brandColor. TypeScript joins the two, so your editor sees one Theme with both.Module augmentation has three rules:
- The name in quotes is the name you import the package by:
"shop-ui". A misspelled name getsInvalid module name in augmentation. - You add to an
interface, the same way two interfaces merge. Atypecan't be added to; it getsDuplicate identifier 'Theme'. - The file must be a module. In a project you usually move the block into its own file, like
src/shop-ui.d.ts, withimport "shop-ui";as its first line. Without animportorexport, the file is a script, and theredeclare module "shop-ui"means something else: it describes the whole package, and TypeScript uses it instead of shop-ui's own types. Then importingcreateThemefails withModule '"shop-ui"' has no exported member 'createTheme'.
Real packages expect this. Emotion, a styling package, gives its Theme interface no properties, and its guide tells you to fill it in with declare module '@emotion/react' in a file that starts with import '@emotion/react'.
Typing environment variables
An environment variable is a setting that comes from outside your code, like the address of the shop's API, which differs between your laptop and the live site. Vite reads them from a file named .env and gives the ones whose names start with VITE_ to your code as import.meta.env.VITE_API_URL.
Vite's own types describe import.meta.env with an interface called ImportMetaEnv. You add your variables to it. This page doesn't have Vite's types, so lines 2 to 5 stand in for them:
declare global { // Vite's own types declare this part (shortened) interface ImportMeta { readonly env: ImportMetaEnv; } // You add your own variables here interface ImportMetaEnv { readonly VITE_API_URL: string; }} const apiUrl = import.meta.env.VITE_API_URL;// ^?const shopUrl = import.meta.env.VITE_SHOP_URL;Lines 3 to 5 say that import.meta has an env of type ImportMetaEnv. Lines 7 to 9 add VITE_API_URL, a string. readonly means your code can read it but not change it. The query on line 13 shows const apiUrl: string. Line 14 gets an error on purpose: Property 'VITE_SHOP_URL' does not exist on type 'ImportMetaEnv'. Did you mean 'VITE_API_URL'?
In a Vite project you write only lines 7 to 9, without declare global, in src/vite-env.d.ts. That file must stay a script: Vite's guide warns that an import in it breaks the addition, because the interface would then belong to that file only. One more difference: Vite's real ImportMetaEnv lets any other name through as any, so a misspelled variable isn't marked unless you turn on its strictImportMetaEnv option.
.env and writes the value into your code when it builds. TypeScript reads only the type in vite-env.d.ts. It never opens .env, so it can't tell whether the variable is really set.Next.js and Node read variables from process.env instead. Node's types (@types/node) describe it with an interface called ProcessEnv inside a namespace called NodeJS, where every name is string | undefined. You add your own names to that interface the same way, inside declare global. Exercise 6 has you do it.
These types are promises you make to TypeScript. It doesn't open .env. If the variable isn't set, Vite writes undefined into the code where you used it, and TypeScript still says string.
One types file for a shop's globals
Real projects keep these declarations in one place, so everyone knows where to look. Here is a types file for the shop: the version from the build, the analytics script, and the optional chat.
export {}; type Analytics = { track(event: string): void };type Chat = { open(): void }; declare global { const __APP_VERSION__: string; var analytics: Analytics; interface Window { chat?: Chat; }} analytics.track(`checkout_v${__APP_VERSION__}`);window.chat?.open();- Line 1,
export {};, exports nothing. It's there to make the file a module, so that thedeclare globalblock on line 6 is allowed. In your project, this file could besrc/globals.d.ts. - Lines 3 and 4 name the shapes of the two scripts' objects. They stay in this file.
- Line 7 declares the build's version. Line 8 uses
var, soanalyticsalso works aswindow.analytics. Lines 10 to 12 add the optional chat toWindow. - Lines 15 and 16 stand in for the rest of your code, which in a project lives in other files. This page checks one file at a time, so they share it here. Line 15 prints
Tracked checkout_v1.4.0. Line 16 does nothing, because this page has no chat.
The table below is a short guide. Read each row as: what you want to add, what to write, and where it goes.
| You want to add | Write | Where it goes |
|---|---|---|
A global from a <script> tag | declare const analytics: Analytics; | In a script file like globals.d.ts, or inside declare global in a module. |
A property on window | interface Window { chat?: Chat } | Inside declare global, or in a script file. |
| A field on a package's interface | declare module "shop-ui" { interface Theme { … } } | In a module: a file with an import or an export. |
| Your Vite variables | interface ImportMetaEnv { readonly VITE_API_URL: string } | In src/vite-env.d.ts, a script file with no imports. |
analytics.track("buy");declare const analytics: { track(event: string): void }; analytics.track("buy");declare global { const cartCount: number } window.cartCount = 3;declare global { var cartCount: number } window.cartCount = 3;declare global { interface Window { cartCount?: number } } window.cartCount = 3;declare const cartCount: number = 3;declare module "shop-iu" { interface Theme { brandColor: string } }declare global { interface Window { chat?: { open(): void } } } window.chat?.open();
Sort each line by what TypeScript says about it. Each line is checked on its own, as a module.
Common mix-ups
- "declare creates the variable." No. It only describes a name, and it's removed before the code runs. If the script never loads, the code still fails.
- "TypeScript checks that the real global matches my declaration." No. It can't see the page while it runs. It trusts what you wrote, the same way it trusts a type on
JSON.parse. - "Inside declare global, const and var are the same." No. Only a
varis also a property ofwindow. Withconst,window.cartCountis an error. - "?. protects a bare global name." No. A name that doesn't exist throws a
ReferenceErrorbefore?.runs. Read it aswindow.chat?.open(). - "declare module "shop-ui" always adds to the package." No. Only in a module. In a file with no
importorexport, it replaces the package's types. - "I can add to a package's type alias." No. Only interfaces merge. A second
typewith the same name is aDuplicate identifiererror. - "Typing VITE_API_URL makes sure it's set." No. TypeScript never reads
.env. A missing variable isundefinedwhen the page runs.
Practice exercises
Each exercise is TypeScript. Press Edit & run on the starter code to try a fix: the errors update as you type, and the console shows what it prints. The page's stand-ins for analytics, gtag, __APP_VERSION__, and process are there too. Try a guess first, then use the hints.
A page with two analytics scripts. Which line will TypeScript mark?
declare const analytics: { track(event: string): void };
analytics.track("add_to_cart");
gtag("event", "checkout");declare const analytics: { track(event: string): void };declare function gtag(command: "event", name: string): void; analytics.track("add_to_cart");gtag("event", "checkout");Line 4 calls gtag, which nothing declares, so TypeScript says Cannot find name 'gtag' there. The fix adds declare function gtag(…) on line 2. Both calls then check, and the code prints Tracked add_to_cart and gtag: event checkout.
After TypeScript removes the types, how many lines of code does the browser get?
declare const __APP_VERSION__: string;declare function gtag(command: "event", name: string): void; gtag("event", `version_${__APP_VERSION__}`);declare const __APP_VERSION__: string;declare function gtag(command: "event", name: string): void; // The browser gets only the next line:gtag("event", `version_${__APP_VERSION__}`);Both declare lines only describe names, so both are removed, and so is the empty line. The browser gets one line: the gtag call, which prints gtag: event version_1.4.0.
TypeScript marks line 5. Fix the declaration, not the code that uses it. What does the fixed code print?
declare global { const shopName: string;} window.shopName = "Tea House";console.log(shopName);declare global { var shopName: string;} window.shopName = "Tea House";console.log(shopName);A global var is also a property of window, so window.shopName = "Tea House" checks. Setting window.shopName makes the global, so line 6 prints Tea House.
The chat script may be blocked, and this page doesn't have it. Fix line 9 so the code never throws. What does it print then?
type Chat = { open(): void }; declare global { interface Window { chat?: Chat; }} window.chat.open();console.log("Help requested");type Chat = { open(): void }; declare global { interface Window { chat?: Chat; }} window.chat?.open();console.log("Help requested");window.chat?.open() skips the call when the chat script didn't load, so the error is gone and line 10 always runs. This page has no chat, so it prints Help requested.
Write a declaration for gtag on line 1 so lines 3 and 4 check, but the misspelled event on line 5 is still marked. Which line is left with an error?
// Write the declaration for gtag here gtag("event", "add_to_cart");gtag("event", "checkout");gtag("event", "chekout");declare function gtag(command: "event", name: "add_to_cart" | "checkout"): void; gtag("event", "add_to_cart");gtag("event", "checkout");gtag("event", "chekout");The name parameter accepts only "add_to_cart" or "checkout", a union of two literal types. Lines 3 and 4 check. Line 5 stays marked: Argument of type '"chekout"' is not assignable to parameter of type '"add_to_cart" | "checkout"', the typo you wanted to catch. It's still marked on purpose in the solution.
Lines 1 to 9 stand in for Node's types. Line 11 is marked. What type does process.env.NEXT_PUBLIC_API_URL have there? Then add your variable so line 11 checks.
// Stands in for what @types/node declares (shortened)declare global { namespace NodeJS { interface ProcessEnv { [name: string]: string | undefined; } } var process: { env: NodeJS.ProcessEnv };} const apiUrl: string = process.env.NEXT_PUBLIC_API_URL;console.log(apiUrl);// Stands in for what @types/node declares (shortened)declare global { namespace NodeJS { interface ProcessEnv { [name: string]: string | undefined; } } var process: { env: NodeJS.ProcessEnv };} // Your additiondeclare global { namespace NodeJS { interface ProcessEnv { NEXT_PUBLIC_API_URL: string; } }} const apiUrl: string = process.env.NEXT_PUBLIC_API_URL;console.log(apiUrl);The second declare global block adds NEXT_PUBLIC_API_URL: string to ProcessEnv, and the two interfaces merge. Now that one name is a string, so line 20 checks, and line 21 prints https://api.example.com. Other names are still string | undefined.
Check your understanding
For each question, keep two things apart: what TypeScript knows from your declarations, and what really exists on the page when it runs. The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7What does
declare const analytics: Analytics;add to the JavaScript the browser runs?Choose an answer to see the explanation.
Question 2 of 7Which line does TypeScript mark in this cart count code?
Read the code, then predictdeclare global { const cartCount: number; } console.log(cartCount); console.log(window.cartCount);Choose an answer to see the explanation.
Question 3 of 7An ad blocker stopped the chat script. What does this code print?
Read the code, then predictdeclare global { interface Window { chat?: { open(): void }; } } window.chat?.open(); console.log("Help requested");Choose an answer to see the explanation.
Question 4 of 7Your
theme.d.tsholds adeclare module "shop-ui" { … }block and has noimportorexport. What happens?Choose an answer to see the explanation.
Question 5 of 7With shop-ui installed, what does TypeScript say about this addition?
Read the code, then predictimport { createTheme } from "shop-ui"; declare module "shop-ui" { type Theme = { brandColor: string; }; }Choose an answer to see the explanation.
Question 6 of 7You add
readonly VITE_API_URL: stringtoImportMetaEnvbut forget to put it in.env. What happens?Choose an answer to see the explanation.
Question 7 of 7Where can a
declare global { … }block go?Choose an answer to see the explanation.
Key takeaways
- TypeScript only knows names your code declares. A global from a script tag or a build tool needs a
declareline. declaredescribes; it never creates. It's removed before the code runs, and it can't hold a value.- In a module, use
declare global { … }to add names every file sees. Only avarthere is also onwindow. - For a script that may not load, add an optional property to
Windowand call it withwindow.chat?.open(). declare module "shop-ui"in a module adds to a package's interfaces. In a script file, it replaces them.- Add your environment variables to
ImportMetaEnvorNodeJS.ProcessEnv. TypeScript still never checks.env.
Remember the one-liner.
Declarations tell TypeScript what will exist when the page runs; making it exist is still your page's job.
Coming next: Strict mode options. It turns on TypeScript's checks one flag at a time, and shows which extra checks are worth adding.