Guide
Build a config checker
Check a JSON config and tell the user what needs fixing.
On this page
Before you start
- Bun and an empty TypeScript project.
- Install @hulla/control using the quick start.
A config file can fail twice: the text might not be JSON, or the JSON might contain the wrong values. You’ll give both failures one return type, then let the CLI decide how to report them.
Create src/config.ts. JSON.parse() throws on malformed text, so wrap that call with tcf(). Give its output type unknown: valid JSON isn’t necessarily valid config.
import { err, ok, tcf, type Result } from '@hulla/control'
type Config = { port: number }
function parseJson(source: string): Result<unknown, Error> {
return tcf({
try: (): unknown => JSON.parse(source),
catch: () => new Error('Config must be valid JSON'),
})
}parseJson('{') returns an error result. parseJson('null') succeeds at this stage because null is valid JSON. The next step checks whether the value is useful.
Append readConfig() in the same file. Return an existing failure immediately; then check the successful value before reading its port:
export function readConfig(source: string): Result<Config, Error> {
const parsed = parseJson(source)
if (parsed.isErr()) return parsed
const value = parsed.value
if (typeof value !== 'object' || value === null || !('port' in value)) {
return err(new Error('Config needs a port'))
}
const port = value.port
if (
typeof port !== 'number' ||
!Number.isInteger(port) ||
port < 1 ||
port > 65535
) {
return err(new Error('Port must be an integer from 1 to 65535'))
}
return ok({ port })
}The early return keeps parsing errors intact. Validation failures use err() directly: nothing has thrown, so there is nothing to catch.
Create src/main.ts. map() turns a valid config into a URL while leaving any error branch alone:
import { readConfig } from './config'
const source = process.argv[2] ?? '{}'
const address = readConfig(source).map(({ port }) => `http://localhost:${port}`)
if (address.isErr()) {
console.error(address.error.message)
process.exitCode = 1
} else {
console.log(address.value)
}The CLI owns the exit status and console output. The config module stays useful in a test, web server, or another command because it only returns values.
From the project root, try these inputs:
bun src/main.ts '{"port":3000}'
# http://localhost:3000
bun src/main.ts '{'
# Config must be valid JSON
bun src/main.ts '{}'
# Config needs a port
bun src/main.ts '{"port":0}'
# Port must be an integer from 1 to 65535The first command exits successfully. Each invalid input prints its message and exits with status 1. No stack trace is needed to explain a missing port.
Try returning an extra config field, such as hostname, and update the success mapping. The compiler points out which callers need the new value. Read Results for structured error payloads, or Async results before adding file or network reads.