How to Fix: declare const handled like const during analysis in Turbopack

6 min read

Turbopack is misclassifying declare const as a runtime const, which breaks analysis and can trigger incorrect optimization or module evaluation decisions.

This issue shows up when Turbopack analyzes TypeScript declarations that should exist only in the type system, but treats them like real runtime bindings. In practice, that means a symbol introduced with declare const can be interpreted as if JavaScript will initialize it at runtime, even though TypeScript erases it completely. If you are testing against next@canary and a reproduction like the one linked in the issue, the failure is usually not in your application logic. It is in the bundler’s static analysis model.

Reference reproduction: CodeSandbox repro.

Understanding the Root Cause

In TypeScript, declare const SOME_VALUE: ... defines an ambient declaration. It tells the type checker that a variable exists, but it does not emit JavaScript. That distinction matters because bundlers and compilers often run static analysis to answer questions like:

  • Does this module create a runtime binding?
  • Can this expression be folded at build time?
  • Is this import or export safe to tree-shake?
  • Should this file be evaluated eagerly?

The bug happens when Turbopack’s analysis path treats declare const too similarly to a real const declaration. A real const means there is a runtime variable in the emitted module graph. An ambient declaration means there is only type-level information. If those two are conflated, Turbopack can build the wrong dependency graph or infer the wrong execution semantics.

Technically, the analyzer should distinguish between:

  • Runtime declarations: emitted into JavaScript and available during execution.
  • Type-only declarations: erased before runtime and unavailable to module evaluation.

When that distinction is lost, several incorrect behaviors become possible:

  • An erased declaration is counted as a live binding.
  • Control-flow or constant propagation logic assumes a value exists.
  • Module side-effect analysis becomes inaccurate.
  • Generated optimization decisions differ from what the final JavaScript actually contains.

This is why the bug is subtle: the source code is valid TypeScript, but the bundler’s internal representation is acting as though ambient syntax has runtime presence.

Step-by-Step Solution

Until the Turbopack fix lands, the safest approach is to avoid patterns that force the analyzer to reason about declare const as if it were executable code. The goal is to preserve type safety while making runtime boundaries explicit.

1. Identify ambient declarations used in executable modules

Search for code like this in files that are imported by application runtime code:

declare const __SOME_FLAG__: boolean

if (__SOME_FLAG__) {
  doSomething()
}

This is risky because __SOME_FLAG__ looks like a runtime constant in control flow, but as a declared ambient symbol it does not actually exist in emitted JavaScript.

2. Replace ambient runtime assumptions with explicit runtime values

If the value must exist at runtime, define it through a real source of truth such as process.env, injected config, or a generated module.

const isEnabled = process.env.NEXT_PUBLIC_SOME_FLAG === 'true'

if (isEnabled) {
  doSomething()
}

This works because Turbopack can analyze a genuine runtime expression instead of a type-only declaration.

3. If the symbol is type-only, move it out of runtime control flow

Sometimes declare const is used only to express external typing. In that case, keep it away from executable branching and initialization logic.

declare const externalValue: string

type ExternalValue = typeof externalValue

export type ConfigShape = {
  key: ExternalValue
}

Here the declaration stays in the type domain only, which avoids misleading the bundler into treating it as runtime state.

4. Prefer generated modules over ambient globals

If your build depends on compile-time constants, create a real module that exports them.

// generated/config.ts
export const FEATURE_FLAG = false
// consuming file
import { FEATURE_FLAG } from './generated/config'

if (FEATURE_FLAG) {
  doSomething()
}

This is much more robust for Turbopack, Webpack, and any future analyzer because it preserves a real runtime binding.

5. Isolate repro logic to confirm the bug

To verify you are hitting this specific issue and not a different TypeScript or Next.js problem:

// suspect pattern
declare const SHOULD_RUN: boolean
export const value = SHOULD_RUN ? 'a' : 'b'

Then compare it to:

// safe runtime equivalent
const SHOULD_RUN = false
export const value = SHOULD_RUN ? 'a' : 'b'

If the behavior changes only when declare is involved, you are very likely seeing the exact ambient-declaration analysis bug from the issue.

6. Use a temporary workaround in Next.js projects

If you need to unblock development before the Turbopack patch is released, choose one of these practical workarounds:

  • Replace declare const used in runtime code with real constants.
  • Move ambient declarations into .d.ts files and keep them strictly type-only.
  • Avoid referencing ambient declarations in top-level module execution.
  • Temporarily switch the affected workflow away from Turbopack if the bug blocks local development or CI.

Example split:

// globals.d.ts
declare const BUILD_ID: string
// bad: runtime use may confuse analysis
export const currentBuild = BUILD_ID
// better: inject or import a real runtime value instead
export const currentBuild = process.env.NEXT_PUBLIC_BUILD_ID ?? 'dev'

7. Validate after the change

After refactoring, test these paths:

  • Local dev with Turbopack enabled
  • Production build
  • Server and client bundles if the symbol is shared
  • Dead-code elimination behavior if feature flags are involved

The expected result is that the app no longer depends on a binding that exists only in TypeScript declarations.

Common Edge Cases

Ambient globals from third-party tooling

Some libraries, testing frameworks, or legacy build setups expose globals documented as declare const. Those are safe only if a real runtime injector exists. If Turbopack cannot see that injector, analysis may still fail.

.d.ts files mixed with executable modules

If ambient declarations are colocated conceptually with runtime code, developers may accidentally consume them in executable expressions. Keep declaration files separate and treat them as type-only contracts.

Conditional exports and feature flags

Code that relies on top-level constant folding is especially vulnerable. A bundler that misreads ambient symbols can produce incorrect branch selection or preserve code that should be removed.

Server-only versus client-only assumptions

A value that is injected on the server may not exist on the client. Even if TypeScript accepts a declare const, Turbopack still needs a real runtime source in each target environment.

Confusion with const enum and type erasure

This bug is specifically about declare const, but similar misunderstandings happen when developers assume every TypeScript construct survives compilation. Anything erased by the compiler must not be relied on as a runtime binding unless another mechanism provides it.

FAQ

1. Why is declare const different from normal const?

declare const is an ambient declaration. It informs TypeScript about a symbol’s shape, but it emits no JavaScript. A normal const creates a real runtime variable.

2. Can I safely use declare const in a Next.js app?

Yes, but only for type-level usage or when a real runtime provider guarantees the symbol exists. Do not assume Turbopack can infer that from the declaration alone, especially in optimization-sensitive code paths.

3. What is the best workaround before Turbopack is fixed?

The most reliable workaround is to replace ambient declarations used in runtime logic with explicit runtime exports, environment variables, or generated config modules. That gives the bundler a concrete binding to analyze.

Bottom line: this issue is not really about TypeScript syntax being wrong. It is about Turbopack analysis treating a type-erased symbol like a runtime constant. The fix is to make runtime values explicit and keep declare const confined to type-only contexts until the upstream patch fully separates ambient declarations from executable bindings.

Leave a Reply

Your email address will not be published. Required fields are marked *