lightsoutAlpha

multi-export

more than one export in a file, outside the closed exception list

Deterministic checkadvisory by defaultbasecode

The argument

  • Each exported function, class, interface, type, or constant has its own file, named after the export (cased per the package's file-naming convention)
  • Non-exported items (private helpers, local types) may co-locate with the export they serve

The Closed Exception List

The only cases where a file may contain more than one item — every exception has a mechanical criterion:

#ExceptionCriterion
1Params / ConstructorParams interfacesStays in the file of its function/class; not exported independently
2Private helpersNot exported; called only within this file (see functions.md)
3Discriminated union familiesA union type and its member types share one file when the members exist only as constituents of that union
4Named constant + derived lookup mapA lookup map keyed by the union (Record<MyType, …>) may live in the const object's file (see named-constants.md)
5A type + the single value typed by itinterface Config beside export const defaultConfig: Config share one file, filed under the value's name — the default has no consumer the type does not already have

Multiple Exported Items — Still Not Negotiable

Invalid rationalizations: "the interface is only used by this constant", "they're closely related", "it's just a small helper" (if it's a helper, make it non-exported — exception 2; if exported, own file).

// ❌ config.ts: export interface Config + export const defaultConfig — split them:
// common/types/Config.ts        → export interface Config { ... }
// common/constants/defaultConfig.ts → export const defaultConfig: Config = { ... }

Exception 3 in practice — a union family shares one file because the members exist only as constituents:

// common/types/SyncEvent.ts
export interface FileAddedEvent {
	kind: typeof SyncEventKind.FileAdded; // discriminant references the const object, never a raw literal
	path: string;
}

export interface RecordParsedEvent {
	kind: typeof SyncEventKind.RecordParsed;
	recordId: string;
}

export type SyncEvent = FileAddedEvent | RecordParsedEvent;

If a member type starts being used independently of the union, it moves to its own file.

One Exported Function Per File — Not Negotiable

Every exported function gets its own file, named after the export (cased per file-naming.md). Rationalizations that are NOT valid: "closely related", "both config functions", "over-engineered to split", "one is just a helper for the other" — if it's truly a helper, make it non-exported and co-locate it; if it's exported, it gets its own file.

// ❌ config.ts exporting loadConfig AND saveConfig — split into loadConfig.ts + saveConfig.ts

Grouping Values: One Named Object, Never a Bag

When several values form one concept — a feature's thresholds, a set of retry defaults — export one named object:

// featureThresholds.ts
export const featureThresholds = { maxBatchSize: 20, maxRetries: 3 } as const;

That is one export (no exception needed), the group has a name at every use site, and it greps. What stays banned is the bag: a constants.ts with a dozen loose exports is unnamed by its file, invisible to search, and grows forever.

The proof

failsrc/common/types/Config.ts
export interface Config {
	name: string;
}

export const maxRetries = 3;
pass
import type { Config } from '@/common/types/Config';

export const defaultConfig: Config = { name: 'default' };

Turn it down

Both lines go in your lightsout.config.json.

"standards-checks": { "multi-export": "advisory" }
"standards-checks": { "multi-export": "off" }