Специальные значения в 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 == undefined — true, но null === undefined — false. Если код где-то делает 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), — пиши преобразование. Если выбор есть — подумай, не лучше ли структурный вариант.