TS2403Semantic Error
Since TS 1.0

Fix TS2403: Subsequent Variable Declarations Must Match

Learn why TypeScript throws TS2403 when two var declarations of one name disagree on its type, and how to fix duplicate @types and stale global shims.

error TS2403: Subsequent variable declarations must have the same type. Variable 'X' must be of type 'Y', but here has type 'Z'

What This Error Means

TS2403 fires when the same name is declared with var more than once and the declarations disagree about its type. TypeScript deliberately allows a var to be declared repeatedly — unlike let and const, repeated var declarations merge into one binding. The catch is that merging only works if every declaration describes the same type. The moment the second declaration says something different, the compiler has no way to pick a winner and rejects it.

The check runs over the whole program, not just one file. var is function-scoped, so at the top level of a script file — or inside a declare global block — a declaration lands in the shared global scope alongside the globals from lib.dom, @types/node and every other declaration file in your program. That is why the two declarations the compiler is complaining about often live in files you have never opened.

// The general shape of the error:
// Variable 'process' must be of type '{ env: Record<string, string>; }', but here has type 'Process'.
//          ~~~~~~~                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~             ~~~~~~~
//          the merged name            what the first declaration said              what this one says

Since TypeScript 4.x the diagnostic is followed by a related-information line, 'process' was also declared here., pointing at the other declaration. Editors render it as a second clickable location — that link is usually the fastest route to the fix.

Common Causes

1. Two Copies of @types/node (or Any Duplicated Global Typings)

By far the most common cause, and the one that appears out of nowhere after an npm install. A dependency pins its own version of @types/node, npm nests a second copy under node_modules, and both copies declare the same globals with types that drifted between versions.

// ❌ Broken — node_modules/@types/node/globals.d.ts (the hoisted copy)
interface LegacyRequire {
  (moduleId: string): unknown
}
declare var require: LegacyRequire
// ❌ Broken — node_modules/some-toolkit/node_modules/@types/node/globals.d.ts
interface ModernRequire {
  (moduleId: string): any
  cache: Record<string, unknown>
}
declare var require: ModernRequire
//          ~~~~~~~ Error: Subsequent variable declarations must have the same type.
//                  Variable 'require' must be of type 'LegacyRequire', but here has type 'ModernRequire'.

Nothing in your own source is wrong here, so the fix is a dependency fix, not a type fix:

# ✅ Fixed — collapse the duplicate down to one version
npm ls @types/node          # lists every copy and who asked for it
npm i -D @types/node@20     # pick the version that matches your TypeScript
// ✅ Fixed — and pin it so a transitive dependency cannot reintroduce the second copy
{
  "overrides": {
    "@types/node": "20.10.8"
  }
}

Note that when both declarations sit in .d.ts files, "skipLibCheck": true makes this error disappear. That is a mute button, not a fix — see step 5 below.

2. A Hand-Written Global Shim Colliding With Real Typings

Plenty of projects carry a globals.d.ts written back when the real typings were not installed. The day @types/node arrives, the shim and the real declaration collide.

// ❌ Broken — src/env.ts, in a project that also has @types/node
export {}
 
declare global {
  var process: { env: Record<string, string> }
  //  ~~~~~~~ Error: Subsequent variable declarations must have the same type.
  //          Variable 'process' must be of type '{ env: Record<string, string>; }',
  //          but here has type 'Process'.
}

Delete the shim and augment the interface the real typings already expose. You get the typed environment variables you were after and everything else process offers:

// ✅ Fixed — augment NodeJS.ProcessEnv instead of redeclaring the global
export {}
 
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      API_URL: string
      LOG_LEVEL?: "debug" | "info" | "error"
    }
  }
}
// process.env.API_URL is now string instead of string | undefined
const apiUrl: string = process.env.API_URL

3. A Global Name That Your lib Already Declares

lib.dom.d.ts declares a surprising number of short global names — name, origin, status, length, closed, event. Declare a global of your own with one of those names and TS2403 is immediate. The same collision happens between dom and webworker in one lib array, and between lib and an @types/node version that ships a competing AbortSignal or fetch.

// ❌ Broken — with "lib": ["es2022", "dom"]
export {}
 
declare global {
  var origin: { host: string; protocol: string }
  //  ~~~~~~ Error: Subsequent variable declarations must have the same type.
  //         Variable 'origin' must be of type 'string',
  //         but here has type '{ host: string; protocol: string; }'.
}
// ✅ Fixed — give your global a name the platform does not own
export {}
 
declare global {
  var appOrigin: { host: string; protocol: string }
}

If the collision is between dom and webworker, keep one environment per tsconfig.json and split the worker code into its own project rather than listing both libs.

4. A Real var Redeclaration in a Script File

The original, non-exotic version of the error: the same var declared twice in one file with two different inferred types. It survives mostly in script files — build scripts, seed files, anything without a top-level import or export.

// ❌ Broken — scripts/report.ts
var appTitle = "Dashboard"
var appTitle = 42
//  ~~~~~~~~ Error: Subsequent variable declarations must have the same type.
//           Variable 'appTitle' must be of type 'string', but here has type 'number'.
// ✅ Fixed — one declaration whose type covers both values
let appTitle: string | number = "Dashboard"
appTitle = 42

Better still, use two names. If a value is a string on one line and a number on the next, it is usually two different pieces of data wearing one identifier.

How to Fix It

  1. Read both locations, not just the first one. The message names the merged variable, the type it was fixed to, and the type this declaration tried to give it. The related-information line — 'X' was also declared here. — points at the other declaration. Decide which of the two is authoritative before changing anything.

  2. If either location is inside node_modules, treat it as a dependency problem. Run npm ls @types/node (or whichever package owns the global) to list every copy in the tree. Two versions means a duplicate: pin one with overrides in package.json — resolutions for Yarn — delete node_modules and the lockfile, and reinstall.

  3. Delete your own global shims once the real typings exist. A declare var process, declare var require or declare var __DEV__ that predates the @types package is pure liability. If you only wanted extra fields, augment the existing interface instead — declare global { namespace NodeJS { interface ProcessEnv { ... } } } — which extends the declaration rather than competing with it.

  4. Align lib, types and @types/node with your TypeScript version. An @types/node far newer than your compiler redeclares globals that your lib also ships. Narrow the types array in tsconfig.json to the packages you actually use so unrelated typings stop entering the global scope, and keep one environment — dom or webworker — per project.

  5. Reach for skipLibCheck only to unblock yourself, and never leave it. "skipLibCheck": true stops the compiler checking .d.ts files, so a conflict between two declaration files vanishes — along with every other typing bug in your dependencies. It does nothing for a conflict declared in your own .ts files. Use it to get a build out of the door, then fix the duplicate and remove it.

  6. Prevent the recurrence: one version of each typings package, no hand-written globals. Add the overrides pin for your typings packages, keep a single globals.d.ts that only ever augments existing interfaces, and let npm ls — or a CI check on the lockfile — catch the second copy before your compiler does.

FAQ

What causes TypeScript error TS2403?

TS2403 means the same name is declared with var more than once, and the declarations do not agree on the type. Repeated var declarations are legal — they merge into a single binding — but the merge only works when every declaration gives the name exactly the same type.

In practice the two declarations are rarely in the same file. var at the top level of a script file, and any var inside a declare global block, lands in the global scope shared by your code, your lib files and every @types package in the program. That makes TS2403 a collision report between files far more often than a typo report inside one.

How do I fix TS2403 for 'require' or 'process' from @types/node?

That wording — Variable 'require' must be of type 'NodeRequire', but here has type 'Require' — points at two copies of @types/node, or at a shim of your own sitting next to the real thing.

[object Object]

If there are two copies, pin one version with an overrides entry in package.json and reinstall from a fresh lockfile. If there is only one copy, search your own sources for declare var process or declare var require and delete it — for extra environment variables, augment NodeJS.ProcessEnv instead of redeclaring the global.

Why do I get TS2403 in node_modules right after npm install?

Because the install nested a second copy of a typings package that declares the same globals as the copy you already had, and the two versions describe those globals differently. Your source code did not change; the shape of the global scope did.

Deduplicate first: npm ls <package>, then an overrides pin and a clean reinstall. Setting "skipLibCheck": true also makes the message go away — the compiler simply stops type-checking .d.ts files — but the version skew stays, and so does every other typing bug in your dependencies that the check would have caught. Note that skipLibCheck will not help if one of the two declarations lives in a .ts file of your own; those are still checked.

Related Errors

Practice This

Browse all TypeScript practice challenges to keep sharpening your type-level skills.

Share this reference

Become a TypeScript Pro

Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges