Adopting TypeScript Strict Null Checks as an Architecture Decision
Enabling TypeScript’s strictNullChecks flag adds compile‑time safety for null and undefined values with minimal configuration, requiring runtime validation at trust boundaries and CI enforcement to maintain the guarantee.
22 Jul 2026, 20:19 UTC

Requirements
The team wants to reduce runtime null‑related defects and improve maintainability without adding heavyweight tooling. The change must be detectable in CI, work with existing build scripts, and allow gradual adoption across the codebase.
Smallest Suitable Design
Enable the compiler flag strictNullChecks (or rely on the broader strict flag) in tsconfig.json. This makes the type system treat null and undefined as distinct values and forces explicit handling before they are used.
{
"compilerOptions": {
"strictNullChecks": true
// or "strict": true
}
}
Trust / Data Boundaries
At system boundaries—such as HTTP request bodies, configuration files, or third‑party SDK callbacks—values may legitimately be null or undefined. The architecture therefore adds a validation step:
- Parse incoming data with a runtime schema library (e.g., Zod, io‑ts).
- If validation succeeds, the parsed result is narrowed to non‑null types.
- Internal functions receive these narrowed values and can safely omit null checks.
Example using Zod:
import { z } from 'zod';
const UserPayload = z.object({
id: z.number(),
email: z.string().email(),
// optional fields become nullable after parsing
nickname: z.string().nullable().optional()
});
async function handler(req) {
const parsed = UserPayload.safeParse(req.body);
if (!parsed.success) {
return { status: 400, errors: parsed.error.format() };
}
// parsed.data.email is guaranteed non‑null/undefined
return await createUser(parsed.data);
}
function createUser(user: z.infer<typeof UserPayload>) {
// No need to check user.email for null/undefined here
console.log(user.email.toUpperCase());
}
Operational Checks
- Add
tsc --noEmit(or the project’s build step) to CI pipelines; any new null‑related type error will fail the build. - Enable ESLint rule
@typescript-eslint/strict-boolean-expressionsto discourage accidental truthy checks on potentially nullable values. - Optionally enable
@typescript-eslint/no-non-null-assertionas a warning to track uses of the!operator.
Failure Modes
- Overuse of non‑null assertion (
!): If developers suppress errors with!without justification, the safety guarantee erodes and runtime null errors can reappear. - Inaccurate declaration files: Third‑party packages lacking proper
@typesmay still produce false‑positive null errors, causing noise or prompting inappropriate!usage. - Boundary validation gaps: If runtime validation is omitted or incomplete, internal code may receive unexpected
nullvalues, leading to runtime errors despite compile‑time safety.
Conditions That Would Change the Design
- If the project adopts a language that already treats
nullas a distinct type (e.g., ReasonML, PureScript), the flag would be unnecessary. - If the team decides to move all validation to a contract‑first approach (e.g., OpenAPI with generated strict types), the runtime validation step could be replaced by generated schemas.
- If build times become prohibitive due to increased type‑checking overhead, the team might consider incremental type checking or partitioning the codebase.
Practical Verification
To confirm the flag is active:
- Create a temporary file
checkNull.tswith:
function maybeString(): string | null {
return Math.random() > 0.5 ? 'hello' : null;
}
const result = maybeString();
console.log(result.length); // should trigger a type error
- Run
tsc --noEmit checkNull.ts. The command should exit with a non‑zero status and output similar to:
checkNull.ts:5:22 - error TS2531: Object is possibly 'null'.
- Replace the unsafe line with a safe alternative, e.g.,
console.log(result?.length ?? 0);, and re‑run the command. The build should now succeed.
This demonstrates that the flag correctly guides the developer toward explicit null handling.
Limitations
The flag only catches null/undefined issues that are visible to the type system. Values that cross the boundary as any (e.g., from untyped JavaScript or poorly typed libraries) will not be checked. Mitigate this by:
- Using
--noImplicitAnyalongsidestrictNullChecks. - Applying runtime validation at all external entry points.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.