Lập trình

TypeScript Strict Mode: Catch Runtime Errors Earlier

TypeScript does not make an application safe merely because files end in .ts. Extensive any, ignored nullability, and unchecked assertions still leave defects for users to discover. Strict mode turns the compiler into a more useful design check.

TypeScript strict mode: Cách giảm lỗi runtime từ sớm

TypeScript does not make an application safe merely because files end in .ts. Extensive any, ignored nullability, and unchecked assertions still leave defects for users to discover. Strict mode turns the compiler into a more useful design check.

Enabling it in an older project may reveal hundreds of errors. The code did not suddenly break; the compiler is exposing assumptions that were already unverified.

What does strict enable?

{
  "compilerOptions": {
    "strict": true,
    "noEmit": true
  }
}

The flag enables a family of checks including strictNullChecks, noImplicitAny, strictFunctionTypes, strictPropertyInitialization, and useUnknownInCatchVariables. Future TypeScript releases may add checks to this family, so run type-checking in CI during upgrades.

Use strictNullChecks to remove unsafe assumptions

const user = users.find((item) => item.id === selectedId);
console.log(user.name); // user may be undefined

Do not automatically add !. Model the business rule:

if (!user) {
  throw new Error('Selected user was not found');
}

console.log(user.name);

When a missing value is normal, return User | undefined. When it violates an invariant, validate at the boundary and use the narrower type internally.

Replace any with unknown at boundaries

any disables checking and propagates. Data from an API, storage, or message bus should begin as unknown:

function parseProfile(input: unknown): Profile {
  if (!isProfile(input)) {
    throw new Error('Invalid profile payload');
  }

  return input;
}

A type guard or schema validator narrows the value after runtime validation. TypeScript types disappear during compilation and cannot validate external data by themselves.

Handle caught values safely

try {
  await saveOrder();
} catch (error: unknown) {
  const message = error instanceof Error
    ? error.message
    : 'Unknown error';

  reportFailure(message);
}

JavaScript can throw any value, so checking before reading message is the honest approach.

Model states with discriminated unions

type LoadState =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: User[] }
  | { status: 'error'; message: string };

This avoids contradictory combinations of isLoading, hasError, and hasData. Switching on status narrows the available fields automatically.

Make switches exhaustive with never

function assertNever(value: never): never {
  throw new Error(`Unhandled state: ${JSON.stringify(value)}`);
}

Use this in the default branch of a switch. Adding a new union member without handling it then becomes a compile-time error.

Validate configuration with satisfies

type RouteName = 'home' | 'account';

const routes = {
  home: '/',
  account: '/account',
} satisfies Record<RouteName, string>;

satisfies checks compatibility while preserving the expression's specific inferred type. It works well for route maps, configuration, and complete dictionaries.

Limit assertions

value! and value as SomeType do not add runtime checks. Prefer conditions, optional chaining, assertion functions that actually verify invariants, and validation for external data. Avoid using as unknown as T to bypass a contract.

Migrate an existing project gradually

  1. Create a dedicated type-check command and run it in CI.
  2. Start with strictNullChecks if null errors dominate.
  3. Fix boundary modules such as API clients, parsers, and shared types first.
  4. Replace boundary any with unknown.
  5. Treat current failures as a backlog and allow no new ones.
  6. Enable all of strict, then evaluate flags such as noUncheckedIndexedAccess.
Do not disable strict mode across the project because one dependency has weak types. Isolate the integration behind an adapter or a narrow, documented exception.

Review checklist

  • External data begins as unknown and is validated.
  • Nullable values are checked before use.
  • Unions represent valid states instead of boolean combinations.
  • Assertions are rare, local, and justified.
  • Type-checking runs in CI.
  • TypeScript upgrades include release-note review and strict-error remediation.

References

Discussion

Comments 0

Sign in to comment

You need an account to join the discussion and reply to other readers.

Sign inRegister

No comments yet. Be the first to share your thoughts.