Strictness Settings — strict, noImplicitAny, strictNullChecks

9 min read

The TypeScript for QA course recommended turning on strict: true immediately. That's correct advice for a new project. For a migration, it's wrong — turning on all strict flags at once in a large JavaScript codebase surfaces hundreds of errors simultaneously and makes progress invisible. This lesson covers each strict flag individually, what it catches, and how to enable them in the right order.

What strict: true actually enables

strict: true is a shorthand that turns on a family of flags all at once. You can enable each flag individually instead:

{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "alwaysStrict": true,
    "noImplicitThis": true,
    "useUnknownInCatchVariables": true
  }
}

Setting strict: false and then enabling individual flags one at a time gives you the same end result, with the ability to pause, fix, and measure after each flag.

noImplicitAny — the first flag to enable

noImplicitAny: true requires every function parameter to have an explicit type. Without it, parameters default to any — which disables type checking for that parameter entirely.

// With noImplicitAny: false (migration start)
function getUserLabel(user) { // user is any — no checking
  return user.email; // no error even if user has no email
}
 
// With noImplicitAny: true
function getUserLabel(user) {
// Error: Parameter 'user' implicitly has an 'any' type
}
 
// Fix: add the type
function getUserLabel(user: { email: string; role: string }) {
  return user.email; // now type-checked
}

When you enable noImplicitAny in a 50-file project mid-migration, you'll typically see 50–200 errors — one per untyped parameter, spread across converted files. The fixes are mechanical: add a type annotation to each parameter.

The pattern that helps most: define interfaces for the shapes you use most often, then reference them across multiple functions.

// types.ts — define once
interface TestUser {
  id: string;
  email: string;
  role: "admin" | "member";
}
 
// login.ts, register.ts, profile.ts — reference everywhere
function login(user: TestUser) { ... }
function verifyProfile(user: TestUser) { ... }

strictNullChecks — the second flag to enable

strictNullChecks: true makes null and undefined their own types. Without it, every type silently accepts null — which is how you get TypeError: Cannot read properties of null at runtime.

// With strictNullChecks: false (migration start)
function getUser(id: string): TestUser {
  const user = users.find(u => u.id === id);
  return user; // no error — user could be undefined
}
 
// With strictNullChecks: true
function getUser(id: string): TestUser {
  const user = users.find(u => u.id === id);
  return user;
  // Error: Type 'TestUser | undefined' is not assignable to type 'TestUser'
}

The fixes come in three patterns:

Pattern 1 — widen the return type:

function getUser(id: string): TestUser | undefined {
  return users.find(u => u.id === id);
}

Pattern 2 — assert non-null (use sparingly):

function getUser(id: string): TestUser {
  const user = users.find(u => u.id === id);
  if (!user) throw new Error(`User ${id} not found`);
  return user; // narrowed to TestUser
}

Pattern 3 — optional chaining at the call site:

const email = getUser(id)?.email ?? "unknown";

In QA code, Pattern 2 is usually right for helper functions: if a fixture user is missing, you want a loud, immediate failure — not a silent undefined propagating through the test.

strict: true
  • – Enable first
  • – Forces parameter types
  • – ~50-200 errors on first pass
  • – Enable second
  • – null ≠ string, undefined ≠ User
  • – Prevents runtime null errors
  • – Enable third
  • – Callback parameter checking
  • – Rarely causes errors in QA code
  • Enable with strict: true –
  • Class properties must init –
  • Mainly affects page objects –

Enable flags in this sequence, fixing all errors before moving to the next:

Migration start:     strict: false, noImplicitAny: false
After 30% migrated:  noImplicitAny: true  → fix ~50-200 errors
After 60% migrated:  strictNullChecks: true → fix ~20-100 errors
After 90% migrated:  strict: true → catches remaining flags
Migration complete:   strict: true, allowJs: false

The error counts decrease as you go because each earlier flag forces you to annotate types that the later flags then check. By the time you enable strictNullChecks, most code already has explicit types that make null-safety errors easier to fix.

Handling a flood of errors

When you enable noImplicitAny and see 150 errors, the instinct is to suppress them all and move on. Resist this. A better approach:

Option A — suppress at file level, fix file by file:

// legacy-helper.ts
// @ts-nocheck  ← suppresses all errors in this file temporarily
// TODO: remove once types are added — tracked in #456

Option B — use any explicitly where the implicit version was:

// Explicit any is visible in code review; implicit any is silent
function processLegacyData(data: any) { // visible, intentional
  return data.someField;
}

Explicit any is better than // @ts-nocheck because it's scoped to one variable instead of the whole file. It shows up in searches for any and can be eliminated one parameter at a time.

Option C — use unknown and narrow:

// unknown is stricter than any — forces you to check before using
function processApiResponse(data: unknown) {
  if (typeof data === "object" && data !== null && "id" in data) {
    return (data as { id: string }).id;
  }
  throw new Error("Unexpected response shape");
}

Use unknown in API response handling where you genuinely don't control the incoming shape, and narrow explicitly. Use any only in legacy code you haven't converted yet.

strictPropertyInitialization — the page object flag

This flag catches a common pattern in Playwright page objects where class properties are declared but not initialised in the constructor:

// With strictPropertyInitialization: true
class LoginPage {
  submitButton: Locator; // Error: not initialised in constructor
  
  constructor(private page: Page) {
    // submitButton is never assigned
  }
}
 
// Fix: initialise in the constructor
class LoginPage {
  submitButton: Locator;
  
  constructor(private page: Page) {
    this.submitButton = page.getByRole("button", { name: "Log in" });
  }
}

This is the most common error you'll hit when enabling strict: true on an existing page object library. The fix is always the same: initialise in the constructor.

⚠️ Common mistakes

  • Enabling strict: true from day one and suppressing everything. You end up with a codebase full of // @ts-ignore and any types, and you've paid the tooling cost without gaining the safety. Enable flags incrementally and fix errors properly as you go.
  • Using the non-null assertion operator (!) everywhere to silence strictNullChecks errors. user!.email tells TypeScript "trust me, this is never null." If you're wrong, you get a runtime error. Use it only when you have verified, at the call site, that the value is non-null — not as a shortcut to suppress errors.
  • Not tracking which flags are enabled. If your tsconfig.json doesn't clearly document which strict flags are enabled and why, the next person to edit it won't know if removing a flag is safe. Keep a comment alongside each flag you explicitly set during migration: // enabled 2024-Q2, ~80 errors fixed.

🎯 Practice task

Using the project from the previous lessons (TypeScript installed, some files converted to .ts):

  1. Enable noImplicitAny: true in tsconfig.json.
  2. Run npm run type-check. Count the errors — write the number down.
  3. Fix the errors in the two or three files you've already converted to .ts. Use explicit interface types wherever you were previously relying on implicit any.
  4. Run npm run type-check again. Confirm the error count is lower.
  5. Now enable strictNullChecks: true. Run npm run type-check. For each null-related error, decide: widen the return type, add a null guard, or use optional chaining?
  6. Stretch: find one place where a converted function returns undefined for a not-found case, and the caller doesn't handle it. Apply Pattern 2 from this lesson — throw an explicit error — and confirm TypeScript now considers the return type non-nullable.

The next lesson covers configuring test runners (Cypress, Playwright, Jest) to process TypeScript files, which is the final setup step before you start renaming files in earnest.

// tip to track lessons you complete and pick up where you left off across devices.