Typescript globalThis for browser and Node.js: Element implicitly has an ‘any’ type because type ‘typeof globalThis’ has no index signature

I’ve got a Typescript 5.2.x / Node.js 18.12.x / ESM-based application that is both a website and a CLI with shared business logic. I’m having issues with declaring global types (and declaring them in a way where they work in the browser and in Node.js). Here’s what the application source tree looks like:

user@id app % tree -I 'node_modules|dist*'
.
├── @types
│   └── index.d.ts
├── index.html
├── package-lock.json
├── package.json
├── readme.md
├── src
│   ├── business-logic.ts
│   ├── cli.ts
│   └── web.ts
├── test
│   └── test.business-logic.ts
├── tsconfig.cli.json
├── tsconfig.json
└── tsconfig.web.json

I have one global variable – hello – defined in @types/index.d.ts:

declare global {
  var hello: string | undefined;
}

export {};

VS Code shows compiler errors related to the global variable, though:

Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.ts

vscode

I have tried various solutions which do not resolve this error:

  • Global file name: @types/index.d.ts and not included in tsconfig.json: "types": ["node"] (from above)
  • Global file name: @types/index.d.ts and included in tsconfig.json: "types": ["node", "@types"]
  • Global file name: types/index.d.ts and included in tsconfig.json: "types": ["node", "types"]
  • Global file name: types/global.d.ts and included in tsconfig.json: "types": ["node", "types"]
  • Changing declare global { to declare module global {

Does anyone know how to resolve this? I’ve tried restarting VS Code after making tsconfig*.json changes 🙂 I’ve include the rest of the relevant source files down below for reference:

package.json:

{
  "name": "app",
  "version": "0.0.1",
  "private": false,
  "type": "module",
  "dependencies": {},
  "devDependencies": {
    "typescript": "5.2.2",
    "@types/node": "18.11.18",
    "ts-node": "10.9.1",
    "esbuild": "0.19.2",
    "pkg": "5.8.1"
  },
  "scripts": {
    "start:web": "npm run package:web && python3 -m http.server",
    "package:web": "rm -rf ./dist.web && npx tsc -p tsconfig.web.json && npx esbuild ./dist.web/src/web.js --bundle --outfile=./dist.web/bundle.web.js",
    "start:cli": "node --experimental-specifier-resolution=node --experimental-modules --no-warnings --loader ts-node/esm ./src/cli.ts",
    "package:cli": "rm -rf ./dist.cli && npx tsc -p tsconfig.cli.json && npx esbuild ./dist.cli/src/cli.js --bundle --outfile=./dist.cli/bundle.cli.js && pkg ./dist.cli/bundle.cli.js --out-path ./dist.cli",
    "start:cli:macos": "npm run package:cli && ./dist.cli/bundle.cli-macos",
    "test": "node --experimental-specifier-resolution=node --experimental-modules --no-warnings --loader ts-node/esm ./test/test.business-logic.ts" 
  }
}

tsconfig.json:

{
  "compilerOptions": {
    "target": "es2019",
    "module": "es2022",
    "moduleResolution": "node",
    "lib": ["es2022", "dom"],
    "types": ["node"],

    "rootDir": ".",

    "skipLibCheck": true, // Needed to skip node_modules imports from being included.
    
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "inlineSources": true,
    
    "experimentalDecorators": true,
    "useDefineForClassFields": false,
    "emitDecoratorMetadata": true,
    "esModuleInterop": true,

    // TypeScript development rules:
    "strict": true,
    //"strictPropertyInitialization": false,
    "allowSyntheticDefaultImports": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "noImplicitOverride": true,
    "forceConsistentCasingInFileNames": true
  },
  "exclude": ["**/node_modules"],
  "include": ["src/**/*.ts", "test/**/*.ts"]
}

tsconfig.web.json:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "dist.web"
  },
  "exclude": ["**/node_modules", "src/cli.ts", "test"]
}

tsconfig.cli.json:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "dist.cli"
  },
  "exclude": ["**/node_modules", "src/web.ts", "test"]
}

src/web.ts:

console.log('load web');

globalThis.hello = 'world';

import { respond } from './business-logic';

console.log('hello', respond());

index.html:

<html>
  <body>Test</body>
  <script defer src="https://stackoverflow.com/questions/77427684/dist.web/bundle.web.js"></script>
</html>

src/cli.ts:

console.log('load cli');

globalThis.hello = 'world';

import { respond } from './business-logic';

console.log('hello', respond());

src/business-logic.ts:

export const respond = (): string | undefined => {
  return globalThis.hello;
}

test/test.business-logic.ts:

import { test, before } from 'node:test';
import { strict as assert } from 'node:assert';

import { respond } from '../src/business-logic';

test('test.business-logic', async (t) => {
  before(async () => {
    globalThis.hello = 'test world';
  });

  await t.test('test respond', async (_t) => {  
    assert.equal('test world', respond());
  });
});

Leave a Comment