Build Your Own Bedrock · Part 2
Sign a contract with your compiler
Every guarantee in this series — errors as values, unforgeable types, parse-don't-validate — rests on one assumption: the types tell the truth. TypeScript, out of the box, lets you lie to it four different ways. So step 0 of building your bedrock is not writing code. It's configuration: a contract with the compiler, signed before the first type exists, that neither you nor anyone on your team can quietly break.
The compiler side
tsconfig.json turns on everything TypeScript can check:
// tsconfig.json
{
"compilerOptions": {
// …
"noImplicitAny": true,
"noImplicitThis": true,
"strict": true,
"strictNullChecks": true
}
} strict already implies the others; bedrock spells them out anyway because they are the point. strictNullChecks in particular is load-bearing: Part 3's entire null policy — Maybe<T> = T | null — only means something when the compiler refuses to let a nullable value pass as a non-null one.
The linter side: the four bans
The compiler can't ban its own escape hatches — they're language features. ESLint can. This file is the heart of the contract:
// devops/eslint/strict-ts.json
{
"rules": {
"no-restricted-syntax": [
"error",
{
"selector": "TSNonNullExpression",
"message": "Non-null assertions (`!`) are not allowed. …"
},
{
"selector": "TSAsExpression",
"message": "Type assertions (`as`) are not allowed. …"
},
{
"selector": "TSTypePredicate",
"message": "Type predicates (`is`) are not allowed. …"
}
]
}
} Together with @typescript-eslint/no-explicit-any set to "error" in the root .eslintrc.json, that's four bans. What each one buys you:
-
any— oneanypoisons every expression it touches; inference downstream silently degrades to "whatever". Banning it means every value in the codebase has a real type, all the way down. -
as— an assertion is you overruling the compiler with no runtime evidence.unknownJson as Usertypechecks and then explodes in production. Banned, the only way to turnunknownintoUseris a decoder that actually looks at the data. -
is— a user-defined type predicate is an unchecked promise: the compiler trusts yourx is Usereven if the function body is wrong, and keeps trusting it when the type gains a field next year. Returnbooleanand let narrowing be earned. -
!— the non-null assertion deletes exactly the checkstrictNullChecksexists to force. Bedrock's alternative is Part 3'sMaybeplus one named, greppable escape (throwIfNull).
Law, not culture
The root config wires it together, and one more file bans React hooks on the frontend (importNamePattern: "use*" — that story is Part 14's). Then the scripts make it law:
// .eslintrc.json + package.json
"extends": [
"./devops/eslint/boundaries.json",
"./devops/eslint/strict-ts.json",
"./devops/eslint/strict-react.json",
// …
],
"tsc": "tsc --noEmit",
"lint": "esw ./ --ext .ts --ext .tsx --max-warnings=0" --max-warnings=0 is the signature on the contract. A convention enforced in code review is culture, and culture decays: it goes soft under deadline pressure, it doesn't transfer to new hires, it loses arguments to "just this once". A convention enforced by CI is mechanical law. Nobody argues with a red build, so nobody has to be the police — which is precisely what makes the rest of this series cheap. When Part 5 claims an Opaque value can't be forged, that claim holds because as is banned here, in a JSON file, forever.
In your language
Every ecosystem has a maximum-strictness switch; step 0 is flipping it before the first commit. Rust: #![deny(warnings)] and clippy's unwrap_used lint to make panics greppable. Kotlin: detekt with a !! ban. Swift: SwiftLint's force_unwrapping and force_cast rules. Python: mypy --strict plus Ruff, and treat cast() and type: ignore as the things to ban. Go: golangci-lint with errcheck, so an ignored error fails the build. Elm simply ships this way — no escape hatches were ever in the language.