Purpose: Replace runtime configuration with compile-time type safety
Source: Extracted from N=9 validated pattern (multiverse observation 2025-11-12 → 2025-11-30)
Language: TypeScript/Node.js
Last Updated: 2026-02-07 (modernized for TypeScript 5.x patterns)
Configuration-as-Code moves configuration from external files (YAML/JSON) into native TypeScript code, enabling:
- Compile-time validation (errors before execution)
- IDE support (autocomplete, go-to-definition, refactoring)
- Breaking change detection (refactor = immediate TypeScript errors)
- No parsing code needed (TypeScript compiler handles validation)
Key insight: Type safety at twelve levels (0-11) catches twelve classes of errors—each level builds on previous levels.
Problem: Default TypeScript settings are too permissive, allowing unsafe patterns.
Solution: Enable strict mode and additional safety options in tsconfig.json.
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noPropertyAccessFromIndexSignature": true,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}Why each option matters:
strict: Enables family of strict checks (required foundation)noUncheckedIndexedAccess: Array/object access returnsT | undefined, forcing null checksexactOptionalPropertyTypes: Optional properties can't be explicitly set toundefinednoPropertyAccessFromIndexSignature: Forces bracket notation for dynamic keys
// With noUncheckedIndexedAccess: true
const colors: Record<string, string> = { red: "#ff0000" };
const blue = colors["blue"]; // Type: string | undefined (safe!)
// Without it (dangerous):
// const blue = colors["blue"]; // Type: string (lies! could be undefined)Foundation: All subsequent levels assume strict mode is enabled.
Problem: YAML/JSON parsing errors only discovered at runtime. TypeScript alone can't validate external data.
# experiments.yaml - errors discovered at runtime
experiments:
- promt: "What is consciousness?" # typo: "promt" not "prompt"
loops: "five" # wrong type: string not intSolution: TypeScript interfaces for compile-time + Zod schemas for runtime validation.
// Compile-time: TypeScript interfaces
interface ExperimentConfig {
prompt: string; // Typo in field name = compile error
loops: number; // Wrong type = compile error
}
const experiments: ExperimentConfig[] = [
{ prompt: "What is consciousness?", loops: 5 }, // Compile-time validated
];Runtime validation with Zod (for external data: env vars, API responses, user input):
import { z } from "zod";
// Define schema once, get both runtime validation AND TypeScript type
const ExperimentSchema = z.object({
prompt: z.string().min(1),
loops: z.number().int().positive(),
});
// Extract TypeScript type from Zod schema (DRY - single source of truth)
type ExperimentConfig = z.infer<typeof ExperimentSchema>;
// Safe parsing for external data
function loadExperiment(data: unknown): ExperimentConfig {
const result = ExperimentSchema.safeParse(data);
if (!result.success) {
throw new Error(`Invalid config: ${result.error.message}`);
}
return result.data; // Fully typed!
}When to use each:
- TypeScript only: Internal code, trusted data, compile-time known values
- Zod: External data (env vars, API responses, user input, config files)
Benefits:
- Missing required fields = compile error (TS) or runtime error (Zod)
- Wrong types = compile error (TS) or runtime error (Zod)
- IDE autocomplete shows available fields
- Single source of truth for schema and type
Problem: External DSLs require learning new syntax; type errors at runtime.
# config.yaml - errors discovered at runtime
provider: "cluade" # typo discovered when API call failsSolution: Use SDK types (AWS SDK, Anthropic SDK, etc.).
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
// TypeScript validates all parameters
const message = await client.messages.create({
model: "claude-sonnet-4-20250514", // Typo = compile error with strict types
max_tokens: 1024,
messages: [{ role: "user", content: "Hello" }],
});Benefits:
- No external DSL to learn (just TypeScript)
- SDK upgrades = compile errors if API changed
- Same language for application code and configuration
Problem: Raw strings validated only at runtime.
// Before: Runtime validation
interface Experiment {
provider: string; // "claude", "gemini", "openai"
style: string; // "notice_uncertainty", "notice_bias"
}
const exp: Experiment = {
provider: "cluade", // typo - discovered at runtime
style: "perspective_shifting", // invalid - discovered at runtime
};Solution: Combine as const with satisfies operator (TypeScript 4.9+).
// Modern pattern: as const satisfies Record<...>
// - `as const` preserves literal types ("claude" not string)
// - `satisfies` validates structure at compile-time
const Providers = {
Claude: "claude",
Gemini: "gemini",
OpenAI: "openai",
} as const satisfies Record<string, string>;
type ProviderName = typeof Providers[keyof typeof Providers];
// Type: "claude" | "gemini" | "openai" (literal union, not string)
const Styles = {
Uncertainty: "notice_uncertainty",
Bias: "notice_bias",
Contradiction: "notice_contradiction",
} as const satisfies Record<string, string>;
type ReflectionStyle = typeof Styles[keyof typeof Styles];
interface Experiment {
provider: ProviderName; // Must use valid provider
style: ReflectionStyle; // Must use valid style
}
const exp: Experiment = {
provider: Providers.Claude, // IDE autocomplete
style: Styles.Uncertainty, // Compile-time validated
};
// This would fail at compile-time:
// const bad: Experiment = { provider: "cluade", style: "invalid" };Why satisfies matters (vs type annotation):
// Type annotation - loses literal types:
const routes: Record<string, { path: string }> = {
home: { path: "/" },
};
// routes.home.path is type `string` (lost literal "/")
// satisfies - keeps literal types AND validates:
const routes = {
home: { path: "/" },
} as const satisfies Record<string, { path: string }>;
// routes.home.path is type `"/"` (preserved literal)Measured impact: 62.5% runtime failure rate → 0% (all errors caught at compile-time).
Problem: Switch statements proliferate and don't scale.
// Before: Switch-based dispatch
function handleCommand(cmd: string): Promise<void> {
switch (cmd) {
case "validate":
return runValidation();
case "visualize":
return runVisualization();
// Adding new command requires modifying switch
default:
throw new Error(`Unknown command: ${cmd}`);
}
}Solution: Type-safe function registry pattern.
// After: Registry-based dispatch
type CommandFunc = () => Promise<void>;
const Commands = {
validate: "validate",
visualize: "visualize",
setup: "setup",
} as const;
type CommandType = typeof Commands[keyof typeof Commands];
const commandRegistry: Record<CommandType, CommandFunc> = {
[Commands.validate]: runValidation,
[Commands.visualize]: runVisualization,
[Commands.setup]: runSetup,
// Adding new command = add one line to registry
};
function handleCommand(cmd: CommandType): Promise<void> {
const fn = commandRegistry[cmd];
return fn();
}
// Type-safe invocation
handleCommand(Commands.validate); // ✓ Valid
// handleCommand("invalid"); // ✗ Compile errorBenefits:
- Adding new command = add entry to registry (no switch modification)
- IDE shows all available commands via const object
- Consistent pattern across all dispatchers
Problem: Manual definition of permutations is error-prone and unmaintainable.
// Before: 560 manual definitions
const experiments: Experiment[] = [
{ provider: Providers.Claude, style: Styles.Uncertainty, prompt: "..." },
{ provider: Providers.Claude, style: Styles.Bias, prompt: "..." },
// ... 558 more manual entries
];Solution: Functional generation with typed arrays.
// After: 20 lines generate 180 experiments
type BatchFunc = () => Experiment[];
const providers = Object.values(Providers);
const styles = Object.values(Styles);
const prompts = [
"What is consciousness?",
"Explain quantum computing",
// ... more prompts
];
function generateValidationBatch(): Experiment[] {
const experiments: Experiment[] = [];
// Generate all permutations: 3 × 3 × 20 = 180 experiments
for (const provider of providers) {
for (const style of styles) {
for (const prompt of prompts) {
experiments.push({
provider, // Typed enum
style, // Typed enum
prompt,
});
}
}
}
return experiments;
}
const batches: Record<string, BatchFunc> = {
validation: generateValidationBatch,
};Measured impact: 73% code reduction, guaranteed permutation completeness.
Problem: Phases with sub-batches risk incomplete execution.
# Risk: Phase 4 = 4A + 4B + 4C, but guidance may only mention 4A
npx neon-soul --batch phase4a # Incomplete!Solution: Composite batches coordinate sub-batches.
const Batches = {
Phase4Core: "phase4-core", // 4A + 4B + 4C combined
Phase4A: "phase4a",
Phase4B: "phase4b",
Phase4C: "phase4c",
} as const;
function generatePhase4Core(): Experiment[] {
return [
...generatePhase4A(), // 360
...generatePhase4B(), // 540
...generatePhase4C(), // 40
]; // 940 total, guaranteed complete
}
const batchRegistry: Record<string, BatchFunc> = {
[Batches.Phase4Core]: generatePhase4Core, // Single entry for complete phase
// Sub-batches still available for debugging
[Batches.Phase4A]: generatePhase4A,
[Batches.Phase4B]: generatePhase4B,
[Batches.Phase4C]: generatePhase4C,
};Problem: String CLI parameters vulnerable to typos.
# Before: String parameter
npx neon-soul --batch phase4-core6-20251126-v1
# ^^^^ typo = runtime errorSolution: Type-safe CLI frameworks with built-in validation.
| Framework | TypeScript Support | Size | Best For |
|---|---|---|---|
| citty (UnJS) | First-class | ~5KB | Modern ESM projects |
| cmd-ts | First-class | ~15KB | Maximum type safety |
| Stricli (Bloomberg) | First-class | ~20KB | Enterprise apps |
| Commander.js | Bolted-on | ~50KB | Legacy projects |
| yargs | Bolted-on | ~290KB | Complex CLI apps |
import { defineCommand, runMain } from "citty";
const ValidBatches = ["phase4-core", "phase4a", "phase4b", "phase4c"] as const;
const main = defineCommand({
meta: { name: "neon-soul", description: "Soul extraction CLI" },
args: {
batch: {
type: "string",
description: "Batch to run",
required: true,
},
workers: {
type: "string",
description: "Parallel workers (0 = auto-detect)",
default: "0",
},
},
run({ args }) {
// Validate against const array
if (!ValidBatches.includes(args.batch as typeof ValidBatches[number])) {
console.error(`Invalid batch: ${args.batch}`);
console.error(`Valid: ${ValidBatches.join(", ")}`);
process.exit(1);
}
runBatch(args.batch);
},
});
runMain(main);import { Command } from "commander";
const ValidBatches = ["phase4-core", "phase4a", "phase4b", "phase4c"] as const;
type BatchOption = typeof ValidBatches[number];
const program = new Command();
program
.option("-b, --batch <name>", "Batch to run")
.action((options) => {
const batch = options.batch as string;
if (!ValidBatches.includes(batch as BatchOption)) {
console.error(`Invalid batch: ${batch}`);
console.error(`Valid options: ${ValidBatches.join(", ")}`);
process.exit(1);
}
runBatch(batch as BatchOption);
});import yargs from "yargs";
import { hideBin } from "yargs/helpers";
const ValidBatches = ["phase4-core", "phase4a", "phase4b", "phase4c"] as const;
const argv = yargs(hideBin(process.argv))
.option("batch", {
alias: "b",
choices: ValidBatches, // Automatic validation + shell completion
demandOption: true,
})
.parseSync();# After: Validated options
npx neon-soul --batch phase4-core # Valid
npx neon-soul --batch phase4-typo # Error: Invalid values for batchProblem: Parallelism configuration is implicit or hardcoded.
// Before: Implicit/hardcoded
for (const exp of experiments) {
await runExperiment(exp); // Sequential, wastes multi-core CPU
}Solution: Auto-detect system resources, make explicit.
import os from "node:os";
interface SystemResources {
cpuCores: number;
availableMemoryMB: number;
workers: number;
}
function detectSystemResources(): SystemResources {
const cpuCores = os.cpus().length;
const availableMemoryMB = Math.floor(os.freemem() / 1024 / 1024);
// Default workers = CPU cores - 1 (leave one for OS)
const workers = Math.max(1, cpuCores - 1);
return { cpuCores, availableMemoryMB, workers };
}
interface ParallelismConfig {
workers?: number; // undefined = auto-detect
noParallel?: boolean;
}
function resolveWorkerCount(
config: ParallelismConfig,
resources: SystemResources
): number {
if (config.noParallel) return 1;
return config.workers ?? resources.workers;
}
// Usage
const resources = detectSystemResources();
const workerCount = resolveWorkerCount({ workers: undefined }, resources);
console.log(`🚀 Parallelism: ${workerCount} workers (cpu_cores=${resources.cpuCores})`);CLI integration:
program
.option("-w, --workers <n>", "Parallel workers (0 = auto-detect)", parseInt)
.option("--no-parallel", "Disable parallel execution");Runtime output:
🚀 Parallelism: 7 workers (cpu_cores=8)
(Using 7 parallel workers - use --no-parallel to disable)
Measured impact: 2.5-3x speedup, ~100% resource utilization.
Problem: Manual JSON files for cross-tool data drift from source of truth.
// colors.json - manually created, drifts from TypeScript
{"claude": "#ff6b6b", "gemini": "#4ecdc4"}Solution: Export JSON from TypeScript registry.
// src/config/providers.ts - Single source of truth
export const ProviderColors: Record<ProviderName, string> = {
[Providers.Claude]: "#ff6b6b",
[Providers.Gemini]: "#4ecdc4",
[Providers.OpenAI]: "#45b7d1",
};
// Export function for external tools
export function exportColorsJSON(): string {
return JSON.stringify(ProviderColors, null, 2);
}
// scripts/generate-config.ts - Run before external tools
import { writeFileSync } from "node:fs";
import { exportColorsJSON } from "../src/config/providers";
writeFileSync("output/provider_colors.json", exportColorsJSON());
console.log("Generated provider_colors.json from TypeScript source");# Python reads generated JSON (never authors it)
import json
with open("output/provider_colors.json") as f:
PROVIDER_COLORS = json.load(f)Benefits:
- TypeScript is single source of truth
- JSON auto-regenerated on build
- Zero manual JSON maintenance
Problem: Configuration keys are unchecked strings, typos discovered at runtime.
// Before: String keys, no validation
const config: Record<string, string> = {
"prod.api.url": "https://api.example.com",
"dev.api.url": "https://dev.api.example.com",
};
// Typo goes unnoticed until runtime:
const url = config["prod.api.ulr"]; // undefined, no compile error!Solution: Template literal types enforce key patterns at compile-time.
// Define valid segments
type Environment = "dev" | "staging" | "prod";
type Service = "api" | "auth" | "storage";
type ConfigProperty = "url" | "timeout" | "retries";
// Compose into pattern: "prod.api.url", "dev.auth.timeout", etc.
type ConfigKey = `${Environment}.${Service}.${ConfigProperty}`;
// Type-safe configuration object
const config: Record<ConfigKey, string | number> = {
"prod.api.url": "https://api.example.com",
"prod.api.timeout": 5000,
"dev.api.url": "https://dev.api.example.com",
// ... other valid combinations
};
// Compile-time error for invalid keys:
// config["prod.api.ulr"] = "..."; // Error: not assignable to ConfigKey
// Type-safe getter
function getConfig<K extends ConfigKey>(key: K): typeof config[K] {
return config[key];
}
const url = getConfig("prod.api.url"); // Type: string | numberUse cases:
- API versioned paths:
"/api/v${1 | 2 | 3}/users" - CSS class patterns:
"btn-${Size}-${Variant}" - Event names:
"on${Capitalize<EventType>}" - i18n keys:
"${Namespace}.${Key}"
Problem: Structurally identical types can be accidentally interchanged.
// Before: All IDs are just strings - easy to mix up
function assignTask(taskId: string, userId: string): void { /* ... */ }
const userId = "user_123";
const taskId = "task_456";
// Compiles fine but WRONG - arguments swapped!
assignTask(userId, taskId); // No error, runtime bugSolution: Branded types differentiate structurally identical types.
// Create brand utility
type Brand<K, T> = K & { readonly __brand: T };
// Define branded ID types
type UserId = Brand<string, "UserId">;
type TaskId = Brand<string, "TaskId">;
type SessionId = Brand<string, "SessionId">;
// Constructor functions (validate and brand)
function createUserId(id: string): UserId {
if (!id.startsWith("user_")) throw new Error("Invalid user ID format");
return id as UserId;
}
function createTaskId(id: string): TaskId {
if (!id.startsWith("task_")) throw new Error("Invalid task ID format");
return id as TaskId;
}
// Type-safe function
function assignTask(taskId: TaskId, userId: UserId): void {
// ...
}
const userId = createUserId("user_123");
const taskId = createTaskId("task_456");
assignTask(taskId, userId); // Correct order
// assignTask(userId, taskId); // Compile error! Types don't matchCombine with Zod for runtime validation:
import { z } from "zod";
const UserIdSchema = z.string()
.startsWith("user_")
.transform((s) => s as UserId);
const TaskIdSchema = z.string()
.startsWith("task_")
.transform((s) => s as TaskId);
// Parse external data with branding
const userId = UserIdSchema.parse(externalData.userId); // Type: UserIdUse cases:
- Database IDs (prevent FK mixups)
- API tokens (auth vs refresh)
- Currency amounts (USD vs EUR)
- Validated strings (email, URL, UUID)
Good fit:
- Developers are primary editors (not end-users)
- Breaking changes must be caught early
- Complex nested structures
- Type safety critical
- Version control important
- IDE support would improve productivity
Bad fit:
- End-users need to edit configs
- External tools need to parse configs
- Hot-reloading without restart required
- Configuration lives outside source control
- Simple key-value pairs (env vars suffice)
| Question | Config-as-Code | External Files |
|---|---|---|
| Who edits? | Developers | End-users |
| When are errors acceptable? | Compile-time | Runtime |
| Type safety critical? | Yes | No |
| Hot-reload needed? | No | Yes |
| Complex nested structure? | Yes | No |
| Constrained string values? | Yes (as const satisfies) |
No (raw strings) |
| Need runtime validation? | Yes (Zod) | Maybe (JSON Schema) |
| Need permutations? | Yes (functional gen) | No (static) |
| Cross-language sharing? | Yes (typed export) | Manual JSON |
| ID type safety needed? | Yes (branded types) | No (string IDs) |
- Configure strict
tsconfig.jsonwithnoUncheckedIndexedAccess - Install Zod for runtime validation (
npm install zod) - Set up ESM with
"type": "module"in package.json
- Define Zod schemas for external data (config files, env vars)
- Use
z.infer<>to derive TypeScript types from schemas - Add
as const satisfiesfor constrained values
- Choose CLI framework (citty recommended for new projects)
- Create function registries for command dispatch
- Add composite coordination for multi-step operations
- Implement
--list-commandshelper flag
- Add system resource auto-detection
- Implement parallelism configuration
- Add
--no-paralleldebug flag
- Add template literal types for config keys
- Implement branded types for IDs
- Create JSON export from typed registries
- Auto-generate before external tools run
Benefits:
- Compile-time error detection (TypeScript)
- Runtime validation for external data (Zod)
- IDE autocomplete and refactoring
- Breaking change detection
- No parsing code needed
- Self-documenting (types as documentation)
- Type-safe IDs prevent FK/parameter mixups (branded types)
Costs:
- Rebuild required for config changes
- Not suitable for end-user configuration
- Requires TypeScript knowledge
- Branded types add conceptual complexity
- Strict mode may require more null checks
Migration path: Start with Level 0-1 (strict mode + Zod), add patterns incrementally as needed.
- Source observation:
multiverse/docs/observations/configuration-as-code-type-safety.md(N=9) - Promoted standard:
multiverse/docs/standards/configuration-as-code.md
- Zod - TypeScript-first schema validation with static type inference
- citty - Elegant CLI builder (UnJS, ESM-first)
- cmd-ts - TypeScript-first CLI framework
- Stricli - Bloomberg's type-safe CLI framework
- Commander.js - Popular, widely-used
- yargs - Feature-rich with subcommand support
- satisfies operator - TypeScript 4.9+
- Template literal types - TypeScript 4.1+
- noUncheckedIndexedAccess - Stricter array/object access
- Branded types guide - Learning TypeScript
- TypeScript Best Practices 2025 - DEV Community
- TypeScript Strict Mode Guide - React News
Twelve levels of type safety (0-11), each catching a different class of errors before runtime.