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: true plus noUncheckedIndexedAccess. The latter forces you to handle the case where an array index returns undefined.

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: any is 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
ConstructInstead of
unknown + validationany
`Xundefined`
z.infer<...>a hand-written interface
as consta 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-error with 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.