Files
2025-11-29 18:22:35 +08:00

4.4 KiB

name, description
name description
avoiding-non-null-assertions Avoid non-null assertion operator (!) and use type-safe alternatives instead

Avoiding Non-Null Assertions

The non-null assertion operator (!) is deprecated in modern TypeScript because it bypasses type safety and can lead to runtime errors.

Why Avoid !

  • Bypasses type safety: Tells TypeScript "trust me" without verification
  • Runtime errors: Can cause undefined or null errors at runtime
  • Maintenance burden: Makes refactoring dangerous
  • No protection: Removes TypeScript's main benefit

Modern Alternatives

1. Optional Chaining (?.)

Bad:

const userName = user!.profile!.name;

Good:

const userName = user?.profile?.name;

2. Nullish Coalescing (??)

Bad:

const value = config!.timeout;

Good:

const value = config?.timeout ?? 5000;

3. Type Guards

Bad:

function processUser(user: User | null) {
  console.log(user!.name);
}

Good:

function processUser(user: User | null) {
  if (user !== null) {
    console.log(user.name);
  }
}

4. Early Return Pattern

Bad:

function getUserEmail(userId: number): string {
  const user = findUser(userId);
  return user!.email;
}

Good:

function getUserEmail(userId: number): string | null {
  const user = findUser(userId);
  if (!user) {
    return null;
  }
  return user.email;
}

5. Custom Type Guards

Bad:

function handleValue(value: unknown) {
  return (value as User)!.name;
}

Good:

function isUser(value: unknown): value is User {
  return typeof value === 'object' && value !== null && 'name' in value;
}

function handleValue(value: unknown) {
  if (isUser(value)) {
    return value.name;
  }
  throw new Error('Invalid user');
}

6. Narrowing with in Operator

Bad:

function process(obj: { data?: string }) {
  console.log(obj.data!.toUpperCase());
}

Good:

function process(obj: { data?: string }) {
  if ('data' in obj && obj.data !== undefined) {
    console.log(obj.data.toUpperCase());
  }
}

7. Array Methods with Type Safety

Bad:

const users: User[] = getUsers();
const firstUser = users[0]!;

Good:

const users: User[] = getUsers();
const firstUser = users.at(0);
if (firstUser) {
  console.log(firstUser.name);
}

8. Assertion Functions (TypeScript 3.7+)

Good:

function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
  if (value === undefined || value === null) {
    throw new Error('Value must be defined');
  }
}

function process(value: string | null) {
  assertIsDefined(value);
  console.log(value.toUpperCase());
}

DOM Element Access

Bad:

const button = document.getElementById('submit')!;
button.addEventListener('click', handler);

Good:

const button = document.getElementById('submit');
if (button) {
  button.addEventListener('click', handler);
}

Or with assertion function:

function assertElement<T extends Element>(
  element: T | null,
  selector: string
): asserts element is T {
  if (!element) {
    throw new Error(`Element not found: ${selector}`);
  }
}

const button = document.getElementById('submit');
assertElement(button, '#submit');
button.addEventListener('click', handler);

When Is ! Acceptable?

Only in very rare cases where:

  1. You have exhaustively verified the value exists
  2. There's no other way to express it to TypeScript
  3. You document WHY it's safe

Even then, prefer assertion functions over !.

Migration Strategy

  1. Search for all uses of ! in codebase
  2. Categorize by pattern (DOM access, array indexing, etc.)
  3. Replace with appropriate type-safe alternative
  4. Test thoroughly after each replacement
  5. Enable linting to prevent future uses

Compiler Configuration

Enable strict checks:

{
  "compilerOptions": {
    "strict": true,
    "strictNullChecks": true,
    "noUncheckedIndexedAccess": true
  }
}

Summary

Never use ! operator:

  • Use ?. for optional chaining
  • Use ?? for default values
  • Use type guards for narrowing
  • Use assertion functions when validation is needed
  • Let TypeScript protect you from null/undefined errors