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
I have tried various solutions which do not resolve this error:
- Global file name:
@types/index.d.ts
and not included intsconfig.json
:"types": ["node"]
(from above) - Global file name:
@types/index.d.ts
and included intsconfig.json
:"types": ["node", "@types"]
- Global file name:
types/index.d.ts
and included intsconfig.json
:"types": ["node", "types"]
- Global file name:
types/global.d.ts
and included intsconfig.json
:"types": ["node", "types"]
- Changing
declare global {
todeclare 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());
});
});