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.
Import the file, never a barrel
Section titled “Import the file, never a barrel”Impact: MEDIUM one less file to maintain per folder
- No
index.tsthat 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
exportsallowlist. 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:
export * from "./labeling-rules.ts"export * from "./labeling-rule-errors.ts"
// billing/charge.tsimport { LabelingRules } from "../labeling/index.ts"✅ Correct — import the file that defines it:
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.genalways reads as belonging to the Effect module, and can never collide with a local binding.Option,Duration,RequestandScheduleare all plausible domain names, and theeffectroot 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)
Write the .ts extension
Section titled “Write the .ts extension”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.tssits beside afoo/folder, which feature-first grouping produces by construction. - The compiler enforces it: under
module: nodenextwithallowImportingTsExtensions, an extensionless relative import is TS2835 intsc --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)
Deferred
Section titled “Deferred”- A lint check for barrels and root-barrel imports — trigger: the same mistake is caught in review a second time.