Специальные значения в JavaScript — это null, undefined и NaN. Каждое значит чуть-чуть своё, и иногда число нужно заменить именно на одно из них — чтобы передать смысл «значения нет», «значение неизвестно» или «вычисление не получилось».

Сама задача звучит непривычно: обычно мы преобразуем между обычными типами — число в строку, строку в булево. А тут число «превращается в пустоту». Разберём, зачем такое может понадобиться и как сделать аккуратно.

Когда это нужно

Несколько типичных кейсов из реальной разработки:

  • Сбросить состояние. Лежит счётчик attempts = 3. После успешного логина хочется «обнулить попытки» так, чтобы соседний код, проверяющий if (attempts !== null), увидел: попыток вообще нет. Тут null даёт более явный сигнал, чем 0 (ноль попыток — всё-таки «есть, но нисколько»).
  • Передача в API. Бэкенд ждёт в теле "discount": null, чтобы пометить «скидки нет». Если послать 0 — он трактует как «скидка 0%», что не то же самое.
  • Невычислимое значение. Делишь на ноль или умножаешь на undefined — на выходе NaN. Иногда хочется довести значение до NaN вручную, чтобы дальнейшая арифметика проваливалась по тому же пути — и обработчик ошибки видел знакомое Number.isNaN(result).
  • Совместимость с TypeScript-моделью. Тип поля объявлен как number | null или number | undefined. Чтобы код был валидным, иногда нужно явно скастить значение в null, когда оно «логически отсутствует».

Технически задача всегда сводится к одному: проверить условие на текущем числе — и, если условие сработало, заменить число на специальное значение.

Способ 1: тернарный оператор

Самый компактный вариант — тернарник. Хорошо читается, когда условие короткое.

let attempts = 3;
attempts = attempts > 0 ? attempts : null;
console.log(attempts);  // 3

let zero = 0;
zero = zero > 0 ? zero : null;
console.log(zero);      // null

let nan = NaN;
nan = Number.isNaN(nan) ? null : nan;
console.log(nan);       // null

В первой строке условие — «число положительное»; если да — оставляем как есть, если нет — заменяем на null. Во втором примере та же логика срабатывает на 0: ноль не больше нуля, значит подставляется null.

Способ 2: условный оператор if

Когда условие сложное или после замены нужно ещё что-то сделать (например, залогировать), читабельнее обычный if.

let amount = 0;

if (!amount) {
    amount = undefined;
    console.log('Сумма не задана, отправляем undefined');
}

// для NaN — отдельная ветка, потому что NaN тоже falsy,
// но семантически это другое
let calc = 10 / 0;             // Infinity, не NaN
let bad = 0 / 0;                // NaN
if (Number.isNaN(bad)) {
    bad = null;
    console.log('Вычисление не дало результата');
}

Тут важная тонкость: !amount сработает и для 0, и для NaN, и даже для undefined — всё falsy. Если хочется заменять только NaN, а ноль оставлять как есть, нужно явное Number.isNaN(x) — глобальная isNaN() сначала кастит аргумент в число и может ошибочно вернуть true для строк.

Способ 3: современные операторы ||, ?? и их assign-варианты

С появлением nullish coalescing (??) в ES2020 и logical assignment (||=, ??=) в ES2021 код стал короче. Это уже «преобразование наоборот» — не «число в null», а «null в число», — но в реальной жизни именно эту связку и пишут чаще всего.

// если value null или undefined — подставить дефолт
let count = null;
let final = count ?? 0;       // 0

// то же самое, но с присваиванием
let attempts = undefined;
attempts ??= 10;              // 10

// разница между || и ??:
let x = 0;
let a = x || 5;               // 5    (|| принимает 0 за falsy)
let b = x ?? 5;               // 0    (?? — только null/undefined)

Запомнить разницу просто: || — для всего falsy (включая 0 и ""), ?? — только для null и undefined. Если ноль — валидное значение в твоей логике, и заменять его не нужно, — пиши ??.

Подводные камни

JSON.stringify и undefined. Если поле объекта равно undefined, оно пропадёт из итогового JSON. null сохраняется. Это критично, когда бэкенду нужен явный сигнал «поле есть, значения нет».

JSON.stringify({ price: undefined, discount: null })
// '{"discount":null}'   — поле price исчезло

NaN переживает арифметику. Если в цепочке вычислений где-то всплыл NaN, дальше всё «заражается»: NaN + 1 === NaN, NaN * 0 === NaN. Если этого не хочется, проверяй Number.isNaN() на каждом шаге и заменяй на безопасное значение (часто 0 или null).

Сравнение с ==. null == undefinedtrue, но null === undefinedfalse. Если код где-то делает if (x == null), он отлавливает оба значения одной проверкой. Это редкий случай, когда == читабельнее ===.

TypeScript-строгий режим. При включённом strictNullChecks присвоить null переменной типа number не получится — компилятор остановит. Это та самая причина, по которой в моделях пишут number | null или number | undefined: явно говорим, что «ноль попыток» и «попыток нет» — разные состояния.

Когда лучше не превращать в спецзначение

Не всегда замена — правильное решение. Иногда чище оставить число как есть и завести отдельный флаг.

// плохо: смешали "сколько" и "был ли результат"
let result = computed > 0 ? computed : null;

// лучше: два отдельных поля
let result = { value: computed, ok: computed > 0 };

Объект с явным флагом ok читается лучше: соседний код видит и значение, и метаданные о нём. null в качестве сигнала «всё плохо» работает, но требует памяти контекста — что именно тут означает null. Если выбора нет (тип поля в API диктует number | null), — пиши преобразование. Если выбор есть — подумай, не лучше ли структурный вариант.