4.4 KiB
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
undefinedornullerrors 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:
- You have exhaustively verified the value exists
- There's no other way to express it to TypeScript
- You document WHY it's safe
Even then, prefer assertion functions over !.
Migration Strategy
- Search for all uses of
!in codebase - Categorize by pattern (DOM access, array indexing, etc.)
- Replace with appropriate type-safe alternative
- Test thoroughly after each replacement
- 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