← Курс/null, undefined и strictNullChecks#147 из 257+20 XP

null, undefined и strictNullChecks

Проблема null и undefined в JavaScript

В JavaScript null и undefined можно присвоить любой переменной. Это приводит к «ошибке на миллиард долларов» — TypeError: Cannot read property of null.

// В обычном JS это допустимо:
let user = getUser()  // может вернуть null
user.name             // TypeError если user === null!

strictNullChecks — строгая проверка null

Когда в tsconfig.json включена опция strictNullChecks: true (по умолчанию в strict режиме), TypeScript считает null и undefined **отдельными типами**:

// strictNullChecks: false (старое поведение, не рекомендуется)
let name: string = null    // OK
let age: number = undefined // OK

// strictNullChecks: true (рекомендуется)
let name: string = null    // Ошибка: null не assignable to string
let age: number = undefined // Ошибка: undefined не assignable to number

// Явное объявление nullable типа:
let name: string | null = null      // OK
let age: number | undefined = undefined // OK

Optional chaining — оператор ?.

Оператор ?. позволяет безопасно обращаться к свойствам, которые могут быть null или undefined. Если цепочка прерывается — возвращается undefined вместо ошибки.

interface User {
  name: string
  address?: {
    city?: string
    zip?: string
  }
}

const user: User | null = getUser()

// Без optional chaining — опасно:
const city = user.address.city  // Ошибка если user или address === null

// С optional chaining — безопасно:
const city = user?.address?.city  // undefined если user или address null/undefined

// Работает и с методами:
const upperName = user?.name?.toUpperCase()

// И с массивами:
const firstItem = arr?.[0]

Nullish coalescing — оператор ??

Оператор ?? возвращает правый операнд только если левый равен null или undefined. В отличие от ||, не считает 0, "" или false «отсутствующими» значениями.

const name = user?.name ?? 'Аноним'
// Если user?.name === null или undefined — вернёт 'Аноним'
// Если user?.name === '' — вернёт '' (пустая строка — валидное значение!)

// Сравнение с ||:
const count1 = 0 || 10    // 10 — нежелательно! 0 — валидное число
const count2 = 0 ?? 10    // 0  — корректно

Non-null assertion — оператор !

Оператор ! в конце выражения говорит TypeScript: «я гарантирую что это значение не null и не undefined». Используй с осторожностью — TypeScript тебе поверит.

const element = document.getElementById('app')
// element: HTMLElement | null

element.style.color = 'red'   // Ошибка TS: Object is possibly null

element!.style.color = 'red'  // OK — мы гарантируем что элемент существует

// Лучше: явная проверка
if (element) {
  element.style.color = 'red'  // OK — TypeScript сам сузит тип до HTMLElement
}

Паттерн: сужение null типов

function getLength(str: string | null): number {
  // Вариант 1: if-проверка
  if (str === null) {
    return 0
  }
  return str.length  // TS знает: str: string здесь

  // Вариант 2: оператор ??
  return (str ?? '').length

  // Вариант 3: short-circuit
  return str?.length ?? 0
}

Примеры

Безопасная работа с null и undefined: optional chaining, nullish coalescing, проверки

// Симуляция данных из API (могут быть null/undefined)
const users = [
  { id: 1, name: 'Алексей', address: { city: 'Москва', zip: '101000' } },
  { id: 2, name: 'Мария', address: null },
  { id: 3, name: null, address: { city: 'Казань' } },
  null,
]

function findUser(id) {
  return users[id - 1]  // может вернуть null
}

// === Optional chaining ?. ===
console.log('=== Optional chaining ===')

const user1 = findUser(1)
const user4 = findUser(4)  // null

// Без optional chaining — опасно:
// console.log(user4.name)  // TypeError: Cannot read properties of null

// С optional chaining — безопасно:
console.log(user1?.name)              // 'Алексей'
console.log(user1?.address?.city)     // 'Москва'
console.log(user4?.name)              // undefined (нет ошибки!)
console.log(findUser(2)?.address?.city) // undefined (address === null)

// === Nullish coalescing ?? ===
console.log('\n=== Nullish coalescing ?? ===')

function getUserCity(userId) {
  const user = findUser(userId)
  // ?. + ?? — безопасное получение с дефолтным значением
  return user?.address?.city ?? 'Неизвестный город'
}

console.log(getUserCity(1))  // 'Москва'
console.log(getUserCity(2))  // 'Неизвестный город' (address === null)
console.log(getUserCity(4))  // 'Неизвестный город' (user === null)

// Разница ?? и ||:
const score = 0
console.log(score || 'нет данных')   // 'нет данных' — НЕПРАВИЛЬНО для 0!
console.log(score ?? 'нет данных')   // 0 — ПРАВИЛЬНО

const label = ''
console.log(label || 'без названия')  // 'без названия' — может быть нежелательно
console.log(label ?? 'без названия')  // '' — пустая строка сохраняется

// === Сужение null (narrowing) ===
console.log('\n=== Проверка null перед использованием ===')

function formatName(name) {
  // Явная проверка на null/undefined (как TS narrowing)
  if (name == null) {  // == null ловит и null и undefined
    return 'Имя не указано'
  }
  return name.toUpperCase()  // здесь name точно строка
}

console.log(formatName('алексей'))  // 'АЛЕКСЕЙ'
console.log(formatName(null))       // 'Имя не указано'
console.log(formatName(undefined))  // 'Имя не указано'

// Получение имён всех пользователей с фильтрацией null:
const names = users
  .filter(u => u != null)
  .map(u => u?.name ?? 'Аноним')
console.log('\nИмена пользователей:', names)  // ['Алексей', 'Мария', 'Аноним']