Skip to main content

Why import type Isn't Optional: A Circular Dependency Crash

Tangled cables forming a circular loop
Apr 6, 20262 min readTypeScript, Next.js, Webpack

The Crash

Every page except the homepage threw "Something went wrong!", which is the root error boundary. There were no build errors, no type errors, and no lint warnings; the app compiled cleanly and the homepage rendered fine. Navigate anywhere else, though, and the entire React tree unmounted.

The error was Cannot read properties of undefined (reading 'href').

Tracing the Cycle

The undefined value was BLOG_LINK, a constant exported from utils/constants.ts. It's used everywhere and it's clearly defined, so it had no business being undefined.

The answer turned out to be a five-module circular dependency:

constants.ts ──imports NavLink──→ types/base.ts ──imports blogPageSlugs──→ data/blog.ts
      ↑                                                                         │
      └──────────── blogThumbnails.ts ←── searchIndex.ts ←── CommandPalette ←────┘

constants.ts imported NavLink from types/base.ts as a value import:

import { NavLink } from '@/types/base';

NavLink is a TypeScript interface, so it doesn't exist at runtime, but Turbopack doesn't know that at module evaluation time. A value import tells the bundler "this module has to be evaluated before mine," so Turbopack evaluates types/base.ts, which imports blogPageSlugs from data/blog.ts, which imports blogThumbnails.ts, which imports BLOG_LINK from constants.ts, the very module that hasn't finished initializing yet.

The result is that BLOG_LINK is undefined by the time blogThumbnails.ts reads it.

The homepage survived because its render path didn't touch CommandPalette early enough to trip the cycle; every other page hit it right away.

The One-Line Fix

- import { NavLink } from '@/types/base';
+ import type { NavLink } from '@/types/base';

import type is guaranteed to be erased by TypeScript before the bundler ever sees it, so there's no module evaluation, no dependency edge, and no cycle. I applied the same fix to the two other files that were importing types from types/base.ts as value imports:

// BreadCrumbs.tsx
- import { BreadCrumbsProps } from '@/types/base';
+ import type { BreadCrumbsProps } from '@/types/base';
 
// getPostDetailProps.ts
- import { NavLink } from '@/types/base';
+ import type { NavLink } from '@/types/base';

Three lines changed, and every page renders again.

Why This Wasn't Caught Earlier

Webpack resolved the cycle differently; it happened to evaluate the modules in an order that worked. Turbopack, which Next.js dev mode uses, evaluates more eagerly, so it exposes cycles that webpack silently tolerated. The ESLint import/no-cycle rule I added later would have caught this, but it wasn't on at the time.

The Rule

Always use import type for type-only imports. It's not a style preference, it's a correctness guarantee. Value imports create module evaluation dependencies and type imports don't, and in a codebase with enough modules that distinction is the difference between "it works" and "every page crashes."

TypeScript's verbatimModuleSyntax flag enforces this for you automatically, and if you're not using it yet, the next circular dependency crash will probably talk you into it.