TypeScript strict — why and how
The ZEUS frontend is Next.js 15 + TypeScript. TypeScript only makes sense when it runs in strict mode — otherwise you get the illusion of safety. This lesson shows why strict is mandatory for us and how to work with it.
What strict is
strict: true in tsconfig.json turns on a set of rigorous checks at once —
including strictNullChecks, noImplicitAny, strictFunctionTypes. Without it
null and undefined slip through everywhere, and any quietly multiplies.
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"skipLibCheck": true
}
}
ProfessNet standard: every frontend project has
strict: trueplusnoUncheckedIndexedAccess. The latter forces you to handle the case where an array index returnsundefined.
null and undefined are explicit
In strict mode string cannot be null. If a value can be empty, the type
says so.
// the type explicitly allows the absence of a value
function findProbe(id: string): Probe | undefined {
return probes.find((p) => p.id === id);
}
const probe = findProbe(id);
probe.host; // error — probe may be undefined
if (probe) {
probe.host; // OK — type narrowing
}
The fight against any
any disables TypeScript. Instead, use unknown and narrow the type
explicitly.
// bad — any lets everything through
function parse(raw: any) {
return raw.data.items; // no checking at all
}
// good — unknown forces a check
function parse(raw: unknown): Item[] {
const data = ScanResultSchema.parse(raw); // e.g. zod
return data.items;
}
ProfessNet standard:
anyis forbidden in new code (the ESLint rule@typescript-eslint/no-explicit-any). Boundaries with the outside world (API,localStorage) we validate with a schema (zod) and get a safe type.
Types from a single source of truth
We don't rewrite types by hand. We derive them from the schema or from the backend.
import { z } from "zod";
export const ProbeSchema = z.object({
id: z.string(),
host: z.string(),
forest: z.string(),
});
export type Probe = z.infer<typeof ProbeSchema>; // type = schema
| Construct | Instead of |
|---|---|
unknown + validation | any |
| `X | undefined` |
z.infer<...> | a hand-written interface |
as const | a loose string/number |
Type checking in CI
tsc --noEmit # type-check only, no build
We run the command in CI alongside ESLint. A red tsc blocks the merge.
Tip: don't use
// @ts-ignore. If you must, use// @ts-expect-errorwith a comment — it disappears automatically once the error no longer occurs.
Strict is the difference between "it compiles" and "it works". Enabled from day one of a project it costs little; enabled later it's a painful refactor. That's why in ZEUS it's the standard from the start.