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.
| Error | TS2304: Cannot find name 'variable'. |
|---|---|
| Where it happens | TypeScript - tsc, ts-node, ts-jest, or an editor language server; any framework. Compile time only, never runtime. |
| What it means | You 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
Jump to your case
- 1. The identifier was never imported
- 2. Typo or wrong casing in the name
- 3. Node globals used without @types/node
- 4. DOM globals used with lib missing DOM
- 5. Test globals used without the test types
- 6. A type-only name imported as a value, or not at all
- 7. Name used before its let or const declaration
- 8. The declaring file is outside include or excluded
- 9. A global injected by a bundler was never declared
- 10. A .d.ts turned into a module by an import
- 11. An enum or class used only as a type, not imported
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.
## None of Those? Narrow It Down
- 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).
- 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. - Run
npx tsc --noEmitfrom 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. - Run
npx tsc --listFilesand look for the file that declares the name. Absent meansinclude/excludeis keeping it out of the program (cause 8) — no amount of editing that file will help until it is in. - Run
npx tsc --showConfigand read the resolvedlibandtypesarrays. A missingDOMexplains browser globals; atypesarray that omitsnodeorjestexplains those globals (causes 3, 4, 5). - If the name should come from a
.d.ts, open that file and check for a top-levelimportorexport. If there is one, the file is a module and itsdeclarestatements are not global (causes 9, 10). - Confirm
node_modulesmatches your lockfile:npm ci. A partially installed tree silently drops@types/*packages and produces exactly this error with no other symptom. - If every step above is clean, narrow to one file. Copy the failing line into a fresh
.tsfile 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.
Stop It From Coming Back
- Enable
"strict": trueintsconfig.jsonand runtsc --noEmitas a CI step separate from your bundler. Bundlers like esbuild and Vite strip types without checking them, so TS2304 can sit inmainindefinitely unless something typechecks. - Turn on the
no-undef-equivalent for TypeScript by using@typescript-eslintwith type-aware linting (parserOptions.project), and enable@typescript-eslint/no-unsafe-*rules. They catch unresolved and implicitly-anyidentifiers at edit time instead of at build time. - Keep one
typings/globals.d.tsfor every bundler-injected or runtime-injected global, and never put a top-levelimportin it — use inlineimport('./mod').Typewhen you need a type from elsewhere. This prevents the module/script flip that silently kills all its declarations. - Avoid setting
compilerOptions.typesunless you specifically need to restrict it. An explicit array disables automatic@typesdiscovery, and every future@types/*install becomes a second step people forget. - Commit
tsconfig.jsonwith anincludethat covers tests, scripts, and typings — not justsrc— and use a separatetsconfig.test.jsonwith"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.
댓글
댓글 쓰기