Skip to main content

Примус типів у JavaScript (неявний vs явний)

Примус типів (type coercion) - це перетворення значення з одного типу в інший, автоматичне під час операції або явне, коли ти сам викликаєш функцію конвертації.

Теорія

TL;DR

  • Неявний примус відбувається сам. "5" + 3 дає "53", а не 8.
  • Явний - це коли ти сам пишеш Number(), String() або Boolean().
  • Оператор + конкатенує, якщо хоча б один операнд є рядком. Всі інші математичні оператори перетворюють на число.
  • == запускає примус перед порівнянням. === - ні.
  • Рівно вісім значень є хибними (falsy). Решта правдива, включаючи [] і {}.

Швидкий приклад

javascript
// Неявний: JavaScript вирішує сам "5" + 3; // "53" - рядок перемагає у + "5" - 3; // 2 - рядок стає числом true + 1; // 2 - true стає 1 null + 5; // 5 - null стає 0 // Явний: вирішуєш ти Number("5"); // 5 String(42); // "42" Boolean(0); // false

У "5" + 3 JavaScript бачить рядок і перетворює число. У "5" - 3 оператор віднімання не має рядкового сенсу, тому рядок стає числом.

Три контексти неявного примусу

Неявне перетворення відбувається у трьох ситуаціях.

Рядковий контекст (оператор + з рядком): інше значення стає рядком. "5" + null дає "5null". "5" + {} дає "5[object Object]". Все, що торкається + поряд з рядком, перетворюється на рядок.

Числовий контекст (-, *, /, %, унарний -): операнди стають числами. "6" * "2" дає 12. null стає 0. undefined стає NaN. true стає 1, false стає 0.

Булевий контекст (умовні вирази, !, ||, &&): значення перевіряються як хибні або правдиві. Порожній рядок хибний. Нуль хибний. null, undefined і NaN хибні. Але [] і {} правдиві. Саме це регулярно дивує людей.

Вісім хибних значень

ЗначенняТип
falseBoolean
0Number
-0Number
0nBigInt
""String
nullnull
undefinedundefined
NaNNumber

Все решта правдиве. "0", "false", [], {} - правдиві. Це класичне питання на співбесіді і класичне джерело багів у продакшені.

Інструменти явного примусу

Number(value) перетворює за правилами специфікації. Number("") дає 0, Number(null) дає 0, Number(undefined) дає NaN, Number("abc") дає NaN.

parseInt("42px") зупиняється на першому нечисловому символі й повертає 42. parseFloat("3.14abc") робить те саме для десяткових чисел. Обидва корисні при обробці введення користувача або CSS-значень.

String(value) - безпечний спосіб перетворити що завгодно на рядок. String(null) дає "null", тоді як null.toString() кидає TypeError.

Boolean(value) і подвоєне заперечення !! дають однаковий результат. !! просто коротше у виразах.

Нестроге vs суворе порівняння

== застосовує правила примусу перед порівнянням. === порівнює тип і значення разом, без жодного перетворення. Строгий оператор рівності (===) - стандартний вибір для більшості порівнянь.

javascript
"5" == 5; // true - рядок стає числом null == undefined; // true - спеціальне правило специфікації null == 0; // false - null дорівнює лише null або undefined "" == false; // true - обидва стають 0 [] == false; // true - [] стає "", "" стає 0

Правило null == undefined закладено навмисно. Це єдина пара значень, яка рівна одна одній через == і нічому іншому. Запис if (value == null) ловить обидва варіанти однією перевіркою, і деякі команди використовують це свідомо.

Типові помилки

Ці приклади з'являються на співбесідах. Буквально одні й ті самі кейси.

javascript
[] + []; // "" - обидва стають "", конкатенація [] + {}; // "[object Object]" - [] це "", {} стає рядком {} + []; // 0 - {} тут парситься як блок, +[] це унарний плюс від [] true + true; // 2 - обидва стають 1 "2" + "2" - "2"; // 20 - "2"+"2" це "22", потім "22"-"2" це 20 NaN === NaN; // false - NaN не рівний сам собі typeof NaN; // "number" - так, саме так

Результат {} + [] залежить від контексту. На початку рядка в консолі {} сприймається як блок операторів, тому +[] - це просто унарний плюс від порожнього масиву, що дає 0. Всередині виразу на кшталт var x = {} + [] отримаєш "[object Object]".

Я одного разу витратив годину на дебаг компонента, який відображав 0 замість нічого. Причиною виявився count && <Component />, де count дорівнював нулю. NaN провокує схожі сюрпризи, коли некоректні числові конвертації течуть через обчислення без перевірки.

Де це зустрічається в реальному коді

  • React-умови: count && <Component /> рендерить текст 0, коли count дорівнює нулю. Краще count > 0 && <Component />.
  • URL-параметри: Number(searchParams.get("page")) повертає 0, якщо параметр відсутній. Перевіряй на null до конвертації.
  • Відповіді API: бекенд може надіслати "true" як рядок. Boolean("true") дає true, і Boolean("false") теж. Для рядкових булевих порівнюй через === "true".
  • Поля форм: input.value завжди рядок. Множення перетворює рядки на числа само, але + конкатенує. Викликай Number() явно.

Питання на співбесіді

Q: Чому typeof NaN повертає "number"?
A: NaN визначений у IEEE 754 як числовий тип. Це результат некоректних обчислень: 0/0 або parseInt("abc"). JavaScript слідує цій специфікації.

Q: Яка різниця між null == undefined і null === undefined?
A: null == undefined дає true за спеціальним правилом специфікації. null === undefined дає false, бо типи різні. Це один з небагатьох випадків, де == поводиться передбачувано.

Q: Коли взагалі варто використовувати == замість ===?
A: Основний випадок - if (value == null), що ловить і null, і undefined разом. Поза цим патерном - === скрізь.

Q: Як JavaScript перетворює об'єкт у примітив під час примусу?
A: Спочатку викликає valueOf(), потім toString(). Якщо жоден не повертає примітив, виникає TypeError. Масиви отримують toString() через join(""), тому [] + [] дає "".

Q: Що поверне [] == ![], і чому?
A: true. ![] дає false, бо [] правдивий і заперечується. Далі [] == false: [] стає "", "" стає 0, false стає 0. Отримуємо 0 == 0, що true. Саме такі приклади пояснюють, чому досвідчені розробники уникають ==.

Приклади

Явна конвертація при обробці форм

javascript
function calculateTotal(priceInput, quantityInput) { const price = Number(priceInput.value); const quantity = Number(quantityInput.value); if (isNaN(price) || isNaN(quantity)) { return "Некоректні дані"; } return price * quantity; } // priceInput.value = "10", quantityInput.value = "3" // "10" * "3" = 30 - неявний примус спрацює // "10" + "3" = "103" - але + конкатенує замість додавання

input.value завжди рядок. Множення перетворює рядки на числа само по собі, тому неявний шлях дає правильне число. Але один випадковий + замість * - і замість суми отримуєш конкатенацію. Явний Number() показує намір і ловить NaN до того, як він потрапить у підсумкову суму.

Пастка + в React

javascript
function CartSummary({ items }) { const count = items.length; return ( <div> {/* Баг: рендерить "0" як текст, коли count дорівнює 0 */} {count && <p>Товарів у кошику: {count}</p>} {/* Виправлення: спочатку перетворити на булеве */} {count > 0 && <p>Товарів у кошику: {count}</p>} {/* Або тернарний оператор */} {count ? <p>Товарів у кошику: {count}</p> : null} </div> ); }

Коли count дорівнює 0, вираз 0 && <p>...</p> скорочується до 0, і React рендерить число 0 у DOM. Цей баг регулярно спливає на code review. count > 0 дає булеве значення до виконання &&, тому React отримує false і нічого не рендерить.

Примус при роботі з відповідями API

javascript
async function getUser(id) { const response = await fetch(`/api/users/${id}`); const data = await response.json(); // API повертає: { active: "true", score: "42", role: null } return { id: data.id, active: data.active === "true", // явно, не Boolean("true") score: Number(data.score) || 0, // 0 як fallback якщо NaN role: data.role ?? "guest", // ??, не ||, щоб дозволити 0 і "" }; }

Boolean("false") дає true, бо будь-який непорожній рядок правдивий. Для рядкових булевих з API порівнюй через === "true" напряму. Оператор ?? повертає fallback тільки для null і undefined, не для 0 чи "", тому він безпечніший за || для числових значень за замовчуванням.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?