TS2304: Cannot find name 'variable'. - 10 Causes and Fixes

TS2304: Cannot find name 'variable'. is the TypeScript compiler telling you that an identifier you used has no declaration it can see from that position in the file. The name 'variable' in the message is a placeholder for whatever identifier you actually wrote — window, process, describe, MyType, or a plain typo.

This is a resolution error, not a type-mismatch error. TypeScript is not saying your value is the wrong shape; it is saying there is no binding with that name in lexical scope, no import that brings it in, and no ambient declaration in the program's type environment.

That means there are only three real fix categories: declare it, import it, or widen the type environment (lib, types, @types/*, include). Everything below is a concrete instance of one of those three.

Causes are ordered most common first. Each one has a reproduction you can paste into a file, the fix, and a way to confirm the fix landed. Close-up of hands coding on a laptop, focusing on programming productivity.

ErrorTS2304: Cannot find name 'variable'.
Where it happensTypeScript - tsc, ts-node, ts-jest, or an editor language server; any framework. Compile time only, never runtime.
What it meansYou used an identifier that has no declaration in lexical scope and no ambient declaration anywhere in the program.

The Fast Fix

In most cases the identifier exists somewhere but was never brought into the file. Add the import:

import { variable } from './config';

If the name is a global your runtime provides (process, document, describe) rather than something you wrote, install its types and list them in tsconfig.json:

npm i -D @types/node
{ "compilerOptions": { "types": ["node"], "lib": ["ES2020", "DOM"] } }

If it is neither — the name is genuinely undeclared — declare it, or fix the typo. Unsure which bucket you're in? Start at the diagnostic checklist.

What Is Actually Causing It

1. The identifier was never imported

Reproduce it

// config.ts
export const variable = 42;

// main.ts
console.log(variable); // TS2304: Cannot find name 'variable'.

Why it happens — Every TypeScript file with an import or export is a module with its own scope. Exporting a name from config.ts publishes it, but it does not inject it into main.ts — the consuming file has to ask for it by name.

The fix

// main.ts
import { variable } from './config';

console.log(variable); // resolves

Added the import statement that binds variable in this file's scope.

Confirm it worked — Run npx tsc --noEmit. The error disappears and no new TS2307 (Cannot find module) appears, which would mean the path is wrong.


2. Typo or wrong casing in the name

Reproduce it

const userName = 'ada';

console.log(username); // TS2304: Cannot find name 'username'.

Why it happens — TypeScript identifiers are case-sensitive. username and userName are two unrelated names, and only one of them is declared.

The fix

const userName = 'ada';

console.log(userName); // matches the declaration exactly

Corrected the casing so the reference matches the declared binding.

Confirm it worked — In your editor, Ctrl/Cmd-click the identifier. If it jumps to the declaration, resolution succeeded; if nothing happens, you are still on an unbound name.


3. Node globals used without @types/node

Reproduce it

// server.ts in a project with no @types/node installed
const port = process.env.PORT; // TS2304: Cannot find name 'process'.
console.log(__dirname);        // TS2304: Cannot find name '__dirname'.

Why it happens — process, __dirname, Buffer, and global are Node runtime globals, not part of the TypeScript language. The compiler only knows they exist if a declaration file declares them, and that file ships in @types/node.

The fix

npm i -D @types/node

If compilerOptions.types is set, Node must be listed in it — an explicit types array disables automatic @types inclusion:

{ "compilerOptions": { "types": ["node"] } }

Installed the ambient declarations and made sure the explicit types allowlist does not exclude them.

Confirm it worked — Hover process in your editor — it should resolve to NodeJS.Process. Or check that node_modules/@types/node/package.json exists.


4. DOM globals used with lib missing DOM

Reproduce it

// tsconfig.json
{ "compilerOptions": { "target": "ES2020", "lib": ["ES2020"] } }
// app.ts
const root = document.getElementById('root'); // TS2304: Cannot find name 'document'.

Why it happens — document, window, fetch, and localStorage live in the DOM lib. Writing an explicit lib array replaces the default set, so omitting DOM removes every browser global from the type environment.

The fix

{ "compilerOptions": { "target": "ES2020", "lib": ["ES2020", "DOM"] } }

Added DOM back to the lib array. Only do this for code that actually runs in a browser — on the server, document really is absent at runtime and the error was correct.

Confirm it worked — Run npx tsc --showConfig and confirm DOM is in the resolved lib list, then re-run tsc --noEmit.


5. Test globals used without the test types

Reproduce it

// sum.test.ts with no test types wired up
describe('sum', () => {   // TS2304: Cannot find name 'describe'.
  it('adds', () => {       // TS2304: Cannot find name 'it'.
    expect(1 + 1).toBe(2); // TS2304: Cannot find name 'expect'.
  });
});

Why it happens — Jest and Mocha inject describe/it/expect as globals at runtime, so nothing in your source imports them. The compiler needs the matching ambient declarations to agree that those names exist.

The fix

For Jest:

npm i -D @types/jest
{ "compilerOptions": { "types": ["jest"] } }

For Vitest, enable its globals instead of installing types:

{ "compilerOptions": { "types": ["vitest/globals"] } }

Or skip globals entirely and import them, which needs no types entry:

import { describe, it, expect } from 'vitest';

Supplied the ambient test declarations, or replaced the globals with explicit imports.

Confirm it worked — Run npx tsc --noEmit across the test files. Hovering describe should show a signature rather than any.


6. A type-only name imported as a value, or not at all

Reproduce it

// types.ts
export interface User { id: string }

// main.ts
const u: User = { id: '1' }; // TS2304: Cannot find name 'User'.

Why it happens — Interfaces and type aliases are erased at compile time, which makes it easy to assume they are globally visible. They are not — a type exported from a module must be imported like any other name.

The fix

import type { User } from './types';

const u: User = { id: '1' };

Added an import type for the interface. A plain import { User } also works; import type makes the erasure explicit and survives verbatimModuleSyntax.

Confirm it worked — npx tsc --noEmit passes, and the emitted JavaScript contains no reference to User — proof it was a type-position name all along.


More causes (5 remaining)

7. Name used before its let or const declaration

Reproduce it

function render() {
  console.log(label); // TS2448 / TS2304 depending on scope
  let label = 'hi';
}

Move the declaration into an outer function and the inner reference becomes a plain unresolved name:

function render() {
  console.log(label); // TS2304: Cannot find name 'label'.
}

function other() {
  let label = 'hi'; // declared in a sibling scope, invisible to render()
}

Why it happens — let and const are block-scoped. A declaration inside one function body is not visible from a sibling function, so from render()'s position the name has no binding at all.

The fix

const label = 'hi'; // hoisted to a scope both functions can see

function render() {
  console.log(label);
}

function other() {
  console.log(label);
}

Moved the declaration to the nearest scope that encloses every use site.

Confirm it worked — Ctrl/Cmd-click the reference; it should jump to the single declaration. npx tsc --noEmit reports neither TS2304 nor TS2448.


8. The declaring file is outside include or excluded

Reproduce it

// tsconfig.json
{ "include": ["src/**/*"] }
// scripts/build.ts references a global declared in typings/env.d.ts
console.log(APP_VERSION); // TS2304: Cannot find name 'APP_VERSION'.

Why it happens — Ambient declarations only apply to files inside the program. A .d.ts sitting in a folder that include does not cover is never loaded, so its declare statements contribute nothing.

The fix

{ "include": ["src/**/*", "typings/**/*.d.ts", "scripts/**/*"] }

Widened include so the declaration file and the consuming file are in the same program.

Confirm it worked — Run npx tsc --listFiles | grep env.d.ts (PowerShell: npx tsc --listFiles | Select-String env.d.ts). If the path appears, the file is in the program.


9. A global injected by a bundler was never declared

Reproduce it

// bundler replaces this token at build time
if (__DEV__) {            // TS2304: Cannot find name '__DEV__'.
  console.log('dev build');
}

Why it happens — Build-time define plugins substitute a literal into the output, so the identifier never exists as a declaration anywhere. The bundler and the compiler are separate systems — configuring one tells the other nothing.

The fix

// typings/globals.d.ts — no imports/exports in this file, so it stays a global script
declare const __DEV__: boolean;

Declared the injected global in an ambient .d.ts. Keep that file free of top-level import/export, or it becomes a module and the declare stops being global.

Confirm it worked — npx tsc --noEmit passes and hovering __DEV__ shows const __DEV__: boolean.


10. A .d.ts turned into a module by an import

Reproduce it

// typings/globals.d.ts
import { Logger } from './logger'; // this line makes the file a module

declare const appLogger: Logger;
// main.ts
appLogger.info('up'); // TS2304: Cannot find name 'appLogger'.

Why it happens — A .d.ts with any top-level import or export is a module, and its declarations become module-scoped rather than global. The declare const is still there — it just is not visible to other files any more.

The fix

// typings/globals.d.ts
declare global {
  const appLogger: import('./logger').Logger;
}

export {}; // keeps the file a module, which declare global requires

Wrapped the declaration in declare global and used an inline import type instead of a top-level import, so the binding lands in global scope.

Confirm it worked — From an unrelated file, hover appLogger. It should resolve to Logger rather than showing TS2304.


11. An enum or class used only as a type, not imported

Reproduce it

// status.ts
export enum Status { Active, Idle }

// main.ts
function label(s: Status) { // TS2304: Cannot find name 'Status'.
  return Status[s];
}

Why it happens — Enums and classes occupy both the type and value namespace, which makes a missing import look like a type error. The resolution failure is the same as any other unimported name.

The fix

import { Status } from './status';

function label(s: Status) {
  return Status[s];
}

Added a value import. import type would not work here, because Status[s] reads the enum at runtime.

Confirm it worked — npx tsc --noEmit passes and the emitted JS still contains the Status reference, confirming the value import was required.

Man intently working on computer programming with code displayed on dual monitors in a dimly lit room. ## None of Those? Narrow It Down
  1. Read the exact name in the error message and decide whether you wrote it or a runtime provides it. Yours → an import or declaration problem (causes 1, 2, 6, 7, 11). A runtime's → a type-environment problem (causes 3, 4, 5, 9).
  2. Search the repo for the declaration: grep -rn "variable" src/. Zero hits means the name does not exist and you need to declare it or fix a typo. Hits in another file mean it exists but is not imported here.
  3. Run npx tsc --noEmit from the project root and compare with your editor. If the CLI is clean and only the editor complains, the editor is using a different tsconfig or a stale TypeScript version — reload the language server and check which tsconfig it picked up.
  4. Run npx tsc --listFiles and look for the file that declares the name. Absent means include/exclude is keeping it out of the program (cause 8) — no amount of editing that file will help until it is in.
  5. Run npx tsc --showConfig and read the resolved lib and types arrays. A missing DOM explains browser globals; a types array that omits node or jest explains those globals (causes 3, 4, 5).
  6. If the name should come from a .d.ts, open that file and check for a top-level import or export. If there is one, the file is a module and its declare statements are not global (causes 9, 10).
  7. Confirm node_modules matches your lockfile: npm ci. A partially installed tree silently drops @types/* packages and produces exactly this error with no other symptom.
  8. If every step above is clean, narrow to one file. Copy the failing line into a fresh .ts file in the same directory and compile it alone. Reproducing there points at config or declarations; not reproducing points at scope inside the original file (cause 7).

Why This Error Exists At All

TypeScript checks your program before anything runs, which means it cannot ask the runtime whether a name exists. It has to answer that question purely from the files in the program: lexical scopes it can see, imports you declared, and ambient declarations loaded from lib and @types. If an identifier is absent from all of those, the compiler has no third option. It cannot assume the name will be there — that assumption is exactly what JavaScript does, and the ReferenceError it produces at runtime is the failure TypeScript was built to move earlier.

The design decision underneath is that the type environment is a closed set you configure, not something discovered. lib picks which language and platform globals exist. types and @types/* add runtime globals. include and files decide which .d.ts files get loaded at all. Every one of these is a knob, and each one can be turned to a position where a name that works perfectly at runtime is invisible to the compiler. That is why a passing build and a failing typecheck can disagree so sharply: they consult different sources of truth. When TS2304 appears for a name you know exists, the question is never "does it exist" — it is "which knob removed it from the set".

The second half of the story is module scope. ES modules gave every file its own namespace, and TypeScript follows that rule exactly, including in .d.ts files. This is why a single import line at the top of a declaration file can silently un-globalize every declare in it: the file crossed from script to module, and module-scoped declarations do not leak. declare global exists specifically to punch back through that boundary. Once you see TS2304 as "resolution found nothing in the configured set" rather than "this variable is broken", the whole family becomes one question with three branches — import it, declare it, or widen the environment — and TS2552 (Did you mean...), TS2307 (Cannot find module), and TS2503 (Cannot find namespace) all turn out to be the same question asked about a different kind of name. Software developer typing code on dual monitors at a wooden desk.

Stop It From Coming Back

  • Enable "strict": true in tsconfig.json and run tsc --noEmit as a CI step separate from your bundler. Bundlers like esbuild and Vite strip types without checking them, so TS2304 can sit in main indefinitely unless something typechecks.
  • Turn on the no-undef-equivalent for TypeScript by using @typescript-eslint with type-aware linting (parserOptions.project), and enable @typescript-eslint/no-unsafe-* rules. They catch unresolved and implicitly-any identifiers at edit time instead of at build time.
  • Keep one typings/globals.d.ts for every bundler-injected or runtime-injected global, and never put a top-level import in it — use inline import('./mod').Type when you need a type from elsewhere. This prevents the module/script flip that silently kills all its declarations.
  • Avoid setting compilerOptions.types unless you specifically need to restrict it. An explicit array disables automatic @types discovery, and every future @types/* install becomes a second step people forget.
  • Commit tsconfig.json with an include that covers tests, scripts, and typings — not just src — and use a separate tsconfig.test.json with "types": ["jest"] so test globals do not leak into production source.
  • Pin TypeScript as a devDependency and point your editor at the workspace version (in VS Code: "TypeScript: Select TypeScript Version" → "Use Workspace Version"). This kills the class of bugs where CI and the editor disagree about what resolves.

Related Guides

Errors You Will Probably Hit Next

  • TS2552: Cannot find name 'variable'. Did you mean 'variables'?
  • TS2307: Cannot find module './config' or its corresponding type declarations.
  • TS2503: Cannot find namespace 'NodeJS'.
  • ReferenceError: variable is not defined

TS2304 is a resolution failure, not a type failure — the compiler searched lexical scope, your imports, and the configured ambient declarations, and found nothing. So the first move is never to change the code at the error line; it is to decide whether the name is yours or the runtime's. Yours means import it or declare it. The runtime's means lib, types, or include is excluding it, and tsc --showConfig plus tsc --listFiles will tell you which one in under a minute.

댓글

이 블로그의 인기 게시물

TypeError: Cannot read properties of undefined (reading 'map') - 11 Causes and Fixes

psql: FATAL: password authentication failed for user - 10 Causes and Fixes

Error: ERR_MODULE_NOT_FOUND: Cannot find package - 10 Causes and Fixes