Skip to content

Imports & barrels

Every file starts with imports, so small choices here repeat thousands of times. This page answers three of them as one set: no barrels, deep-path namespace imports, explicit .ts.

Impact: MEDIUM one less file to maintain per folder

  • No index.ts that re-exports a directory. The "./*": "./src/*.ts" exports wildcard already makes the package name the entry (see workspace boundaries), so a barrel adds a file and nothing else.
  • Barrels exist to back a curated exports allowlist. Drop the allowlist and they stop being needed.
  • Enforced by review, not by a tool: a hand-written barrel is a review comment.
  • Accepted cost: moving a file breaks every import of it, not one barrel line.

❌ Incorrect — a directory re-export standing between caller and file:

labeling/index.ts
export * from "./labeling-rules.ts"
export * from "./labeling-rule-errors.ts"
// billing/charge.ts
import { LabelingRules } from "../labeling/index.ts"

✅ Correct — import the file that defines it:

billing/charge.ts
import * as LabelingRules from "../labeling/labeling-rules.ts"

Source: notes/01-code-organization/imports-and-barrels.md · Decision 1 (amended)

Import Effect modules as namespaces from deep paths

Section titled “Import Effect modules as namespaces from deep paths”

Impact: MEDIUM no collisions with domain names

  • Write import * as X from "effect/X". These are published entry points, not internals.
  • Effect.gen always reads as belonging to the Effect module, and can never collide with a local binding. Option, Duration, Request and Schedule are all plausible domain names, and the effect root is a 139-module barrel.
  • Enforced by review: an import { Effect } from "effect" is a review comment.
  • Accepted cost: long import headers, often ~20 lines before the first line of code.

❌ Incorrect — named imports from the root barrel:

import { Effect, Layer, Option } from "effect"

✅ Correct — one namespace per module, by its own path:

import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import * as SqlClient from "effect/sql/SqlClient"

Source: notes/01-code-organization/imports-and-barrels.md · Decision 2 (amended)

Impact: LOW unambiguous, standard ESM resolution

  • Relative imports end in .ts. That matches native ESM resolution and needs no resolver magic.
  • It is unambiguous when foo.ts sits beside a foo/ folder, which feature-first grouping produces by construction.
  • The compiler enforces it: under module: nodenext with allowImportingTsExtensions, an extensionless relative import is TS2835 in tsc --noEmit. See TypeScript config.

❌ Incorrect — extensionless, could mean a file or a folder:

import * as LabelingRules from "../labeling/labeling-rules" // TS2835

✅ Correct — say exactly which file:

import * as LabelingRules from "../labeling/labeling-rules.ts"

Source: notes/01-code-organization/imports-and-barrels.md · Decision 3 (amended)

  • A lint check for barrels and root-barrel imports — trigger: the same mistake is caught in review a second time.