Примус типів у JavaScript (неявний vs явний)
Примус типів (type coercion) - це перетворення значення з одного типу в інший, автоматичне під час операції або явне, коли ти сам викликаєш функцію конвертації.
Теорія
TL;DR
- Неявний примус відбувається сам.
"5" + 3дає"53", а не8. - Явний - це коли ти сам пишеш
Number(),String()абоBoolean(). - Оператор
+конкатенує, якщо хоча б один операнд є рядком. Всі інші математичні оператори перетворюють на число. ==запускає примус перед порівнянням.===- ні.- Рівно вісім значень є хибними (falsy). Решта правдива, включаючи
[]і{}.
Швидкий приклад
// Неявний: 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 хибні. Але [] і {} правдиві. Саме це регулярно дивує людей.
Вісім хибних значень
| Значення | Тип |
|---|---|
false | Boolean |
0 | Number |
-0 | Number |
0n | BigInt |
"" | String |
null | null |
undefined | undefined |
NaN | Number |
Все решта правдиве. "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 суворе порівняння
== застосовує правила примусу перед порівнянням. === порівнює тип і значення разом, без жодного перетворення. Строгий оператор рівності (===) - стандартний вибір для більшості порівнянь.
"5" == 5; // true - рядок стає числом
null == undefined; // true - спеціальне правило специфікації
null == 0; // false - null дорівнює лише null або undefined
"" == false; // true - обидва стають 0
[] == false; // true - [] стає "", "" стає 0Правило null == undefined закладено навмисно. Це єдина пара значень, яка рівна одна одній через == і нічому іншому. Запис if (value == null) ловить обидва варіанти однією перевіркою, і деякі команди використовують це свідомо.
Типові помилки
Ці приклади з'являються на співбесідах. Буквально одні й ті самі кейси.
[] + []; // "" - обидва стають "", конкатенація
[] + {}; // "[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. Саме такі приклади пояснюють, чому досвідчені розробники уникають ==.
Приклади
Явна конвертація при обробці форм
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
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
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 чи "", тому він безпечніший за || для числових значень за замовчуванням.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.