Питання на співбесіді: Типи й приведення
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
12 питань
===(строга рівність) -true, лише якщо однакові і тип, і значення. Без перетворень.==(нестрога) спершу приводить операнди до спільного типу за досить заплутаними правилами.
1 === '1'; // false
1 == '1'; // true
0 == ''; // true
0 == '0'; // true
'' == '0'; // false - рівність не транзитивна
null == undefined; // true
null == 0; // false
[] == false; // true
NaN === NaN; // false
Правило: завжди === і !==. Більшість лінтерів (правило eqeqeq) так і вимагає.
Єдиний поширений виняток: value == null - коротка перевірка «null або undefined» одночасно. Але й тут читабельніше value === null || value === undefined або оператор ??.
Ще нюанси:
NaNне дорівнює нічому, навіть собі. Перевірка -Number.isNaN(x).- Об'єкти й масиви порівнюються за посиланням:
[1] === [1]-false, бо це два різні масиви. Object.is(a, b)- як===, алеObject.is(NaN, NaN)даєtrue, аObject.is(0, -0)-false.
undefined- «значення немає, бо його не задали»: неініціалізована змінна, відсутня властивість, параметр без аргументу, результат функції безreturn.null- «значення свідомо немає»: його присвоюють явно.
let a; // undefined
const user = {};
user.phone; // undefined
const selected = null; // свідомо «нічого не вибрано»
typeof undefined; // 'undefined'
typeof null; // 'object' - історична помилка мови
|| проти ??:
a || bповертаєb, якщоaбудь-яке хибне значення:0,'',false,NaN,null,undefined.a ?? bповертаєb, лише якщоa-nullабоundefined.
const page = 0;
page || 1; // 1 - помилка: нуль - валідне значення
page ?? 1; // 0
const title = '';
title || 'Без назви'; // 'Без назви'
title ?? 'Без назви'; // ''
Правило: для значень за замовчуванням - ??, якщо 0, '' чи false мають бути збережені.
Поруч живе ?. (optional chaining): user?.address?.city повертає undefined, якщо будь-яка ланка null чи undefined, замість TypeError. І присвоєння ??=: options.timeout ??= 5000.
У JavaScript сім примітивних типів і один об'єктний:
| Тип | Приклад |
|---|---|
string |
'Київ' |
number |
42, 3.14, NaN, Infinity |
bigint |
9007199254740993n |
boolean |
true, false |
undefined |
змінна без значення |
null |
«навмисно порожньо» |
symbol |
Symbol('id') |
object |
об'єкти, масиви, функції, дати, Map... |
Примітиви незмінні й передаються за значенням:
let a = 'текст';
let b = a;
b += '!';
a; // 'текст' - зміна b не торкнулася a
const s = 'привіт';
s.toUpperCase(); // повертає НОВИЙ рядок, s не змінився
Об'єкти передаються за посиланням: змінна зберігає не сам об'єкт, а посилання на нього.
const user = { name: 'Оля' };
const same = user;
same.name = 'Іра';
user.name; // 'Іра' - це той самий об'єкт
function rename(obj) { obj.name = 'Петро'; }
rename(user); // змінює оригінал
Порівняння теж відрізняється: примітиви порівнюються за значенням, об'єкти - за посиланням:
'a' === 'a'; // true
{} === {}; // false - два різні об'єкти
[1, 2] === [1, 2]; // false
Як у примітива є методи? 'текст'.length працює, бо рушій на мить «загортає» примітив у тимчасовий об'єкт-обгортку (String), викликає метод і викидає обгортку. Створювати обгортки вручну (new String('a')) не варто: new String('a') === 'a' - false, а new Boolean(false) у умові - істинний.
Чому це важливо на практиці:
- зміна об'єкта в одній частині коду видна всюди, де є посилання на нього, - джерело багатьох помилок. Звідси звичка створювати копії (
{ ...user },toSorted()) замість змін на місці; - реактивність Vue і стан React покладаються на це: React порівнює стан за посиланням і не помітить зміни всередині того самого об'єкта;
typeofповертає'object'дляnullі масивів - історична особливість, тому для перевірки масиву єArray.isArray.
Значення з полів форми, URL і dataset - завжди рядки. Перетворити їх на число можна кількома способами, і вони поводяться по-різному.
Number(value) і унарний плюс +value - однаково строгі: весь рядок має бути числом.
Number('42'); // 42
Number(' 42 '); // 42 - пробіли по краях ігноруються
Number('42px'); // NaN
Number('3.14'); // 3.14
Number(''); // 0 - пастка!
Number(null); // 0
Number(undefined); // NaN
+'42'; // 42
parseInt(value, radix) читає ціле число з початку рядка й зупиняється на першому недопустимому символі:
parseInt('42px'); // 42
parseInt('3.99'); // 3 - дробова частина відкидається
parseInt('px42'); // NaN
parseInt('ff', 16); // 255
Завжди передавайте основу числення другим аргументом (parseInt(value, 10)) - так намір очевидний, а лінтери цього вимагають.
parseFloat(value) - те саме для дробових: parseFloat('3.14abc') → 3.14.
Що обрати:
- дані з форми, де очікується саме число -
Number(), і перевіряти результат. Він не «пропустить»'12abc'; - значення з одиницями CSS (
'16px') -parseInt/parseFloat; +value- коротко, але менш помітно в коді.
Пастки:
- порожній рядок дає 0, а не
NaN. Порожнє поле «кількість» перетвориться на нуль. Порожнечу треба перевіряти окремо; parseIntз числом, а не рядком:parseInt(0.0000005)→5, бо число спершу перетворюється на рядок'5e-7'. Для округлення чисел єMath.trunc,Math.round;parseIntяк колбекmap:['1', '2', '3'].map(parseInt)дає[1, NaN, NaN], боmapпередає індекс другим аргументом, і він стає основою числення. Правильно:.map(Number)чи.map((s) => parseInt(s, 10));- кома як десятковий роздільник: українські користувачі пишуть
'12,5', аNumber('12,5')- цеNaN. Для введення сум потрібна нормалізація (value.replace(',', '.')) або полеtype="number", значення якого браузер віддає з крапкою.
Перевірка результату: Number.isNaN(n) чи Number.isFinite(n) (відкидає і NaN, і Infinity).
Усі числа в JavaScript (крім BigInt) - 64-бітні числа з плаваючою комою за стандартом IEEE 754. Більшість десяткових дробів, як 0.1, у двійковій системі - нескінченні дроби, тож зберігаються з крихітною похибкою.
0.1 + 0.2; // 0.30000000000000004
0.1 + 0.2 === 0.3; // false
1.005 * 100; // 100.49999999999999
Це не баг JavaScript - так само поводяться float у PHP, Python, Java.
Як порівнювати дроби: з допуском:
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON; // true
Як рахувати гроші:
- У найменших одиницях - копійках, як цілі числа:
1999замість19.99. Цілі числа доNumber.MAX_SAFE_INTEGER(≈ 9 квадрильйонів) представлені точно. - Округлення - явне й в одному місці:
Math.round(price * quantity). - Для складних розрахунків (відсотки, курси, розподіл) - бібліотеки десяткової арифметики (
decimal.js,big.js,dinero.js). - Показ користувачу - через
Intl.NumberFormat:
new Intl.NumberFormat('uk-UA', { style: 'currency', currency: 'UAH' }).format(1999 / 100);
// '19,99 ₴'
toFixed() - для показу, не для розрахунків: він повертає рядок і сам округлює з тією ж похибкою ((1.005).toFixed(2) дає '1.00').
У логічному контексті (if, &&, ||, !, тернарний оператор) кожне значення перетворюється на true чи false. Хибних значень небагато:
false, 0, -0, 0n, '' (порожній рядок), null, undefined, NaN.
Усе інше - істинне, і саме тут сюрпризи:
Boolean('0'); // true - непорожній рядок
Boolean('false'); // true
Boolean([]); // true - порожній масив
Boolean({}); // true - порожній об'єкт
Де це підводить:
- Нуль як валідне значення:
function setVolume(level) {
if (!level) level = 50; // гучність 0 перетвориться на 50
}
Правильно: level ??= 50 чи явна перевірка level === undefined.
- Перевірка порожнього масиву:
if (items)завжди істинне. Потрібноif (items.length > 0). - Рендер у React:
{count && <Badge />}приcount === 0виведе на сторінку0. Правильно:{count > 0 && <Badge />}. - Рядки з форми: значення з
<input>завжди рядок, і'0'- істинний.
!!value чи Boolean(value) явно перетворюють значення на логічне - корисно, коли функція має повертати саме true/false.
NaN (Not a Number) - спеціальне значення типу number, яке означає результат невизначеної чи неможливої числової операції:
typeof NaN; // 'number'
Number('abc'); // NaN
0 / 0; // NaN
Math.sqrt(-1); // NaN
undefined + 1; // NaN
NaN не дорівнює нічому, навіть собі. Так визначено стандартом IEEE 754 для чисел з рухомою комою:
NaN === NaN; // false
NaN == NaN; // false
const x = NaN;
x !== x; // true - єдине значення з такою властивістю
Тому if (value === NaN) ніколи не спрацює.
Як перевіряти правильно:
Number.isNaN(value); // true лише для справжнього NaN
Object.is(value, NaN); // теж true
Не плутати з глобальною isNaN(). Вона спершу перетворює аргумент на число, тож відповідає на інше питання - «чи стане це NaN після перетворення»:
isNaN('abc'); // true - 'abc' перетворився на NaN
Number.isNaN('abc'); // false - це рядок, а не NaN
isNaN(undefined); // true
isNaN(''); // false - '' перетворюється на 0
Для перевірки «чи є це коректним числом» найзручніше Number.isFinite(value): false для NaN, Infinity і будь-яких нечислових типів.
NaN «заражає» обчислення: будь-яка арифметика з NaN дає NaN. Одне некоректне значення в масиві перетворить суму на NaN:
[10, 20, Number('')].reduce((a, b) => a + b); // 30 ('' дає 0)
[10, 20, Number('x')].reduce((a, b) => a + b); // NaN
Звідси правило: перевіряти дані на вході (при парсингу форми чи відповіді API), а не шукати, де з'явився NaN, у кінці розрахунку.
Різні методи по-різному бачать NaN:
[NaN].includes(NaN)→true(алгоритм SameValueZero);[NaN].indexOf(NaN)→-1(строга рівність);new Set([NaN, NaN]).size→1;JSON.stringify(NaN)→'null'-NaNтихо перетворюється наnullпри відправці на сервер.
Об'єкт Date зберігає одну мить у часі - кількість мілісекунд від 1970-01-01 UTC. Часового поясу в ньому немає: методи на кшталт getHours() показують цю мить у поясі браузера користувача, а getUTCHours() - в UTC.
Головні пастки:
1. Місяці з нуля.
new Date(2026, 0, 31); // 31 січня 2026
new Date(2026, 1, 31); // 3 березня - «переповнення» тихо переноситься
2. Розбір рядків залежить від формату.
new Date('2026-10-04'); // північ за UTC (лише дата - як UTC)
new Date('2026-10-04T10:00'); // 10:00 за місцевим часом браузера
new Date('04.10.2026'); // залежить від рушія - не використовувати
Користувач у Києві побачить new Date('2026-10-04').getDate() як 4, а в Нью-Йорку - як 3: північ UTC там ще попередній день.
3. Об'єкт змінюваний.
const start = new Date();
const end = start;
end.setDate(end.getDate() + 7); // змінили і start теж
4. Арифметика місяців. setMonth(month + 1) від 31 січня дає 3 березня (лютого 31-го не існує).
Як працювати безпечно:
- обмінюватися з сервером у ISO 8601 з поясом:
2026-10-04T07:00:00Zчи з зміщенням+03:00. Laravel за замовчуванням серіалізує дати моделей саме так (UTC зZ); - зберігати й передавати мить у UTC, а в місцевий час перетворювати лише для показу;
- показувати через
Intl.DateTimeFormat, а не збирати рядок вручну:
new Intl.DateTimeFormat('uk', {
dateStyle: 'long',
timeStyle: 'short',
timeZone: 'Europe/Kyiv',
}).format(new Date('2026-10-04T07:00:00Z'));
// '4 жовтня 2026 р. о 10:00'
- «дата без часу» (день народження, дата події) - не перетворювати на
Dateвзагалі, а тримати рядком'2026-10-04', інакше часовий пояс зсуне її на день; - відносний час -
Intl.RelativeTimeFormat('uk')(«3 дні тому»).
Що далі. Новий API Temporal розділяє поняття: Temporal.PlainDate (дата без часу й поясу), Temporal.ZonedDateTime (час у конкретному поясі), Temporal.Instant (мить) - і робить їх незмінними. Він поступово з'являється в браузерах; до повної підтримки - поліфіл або бібліотеки (date-fns, Day.js, Luxon).
Універсального способу немає - для різних типів різні інструменти.
typeof - для примітивів і функцій:
typeof 'x'; // 'string'
typeof 1; // 'number' (і для NaN теж)
typeof 1n; // 'bigint'
typeof undefined; // 'undefined'
typeof Symbol(); // 'symbol'
typeof (() => {}); // 'function'
typeof null; // 'object' - історична помилка
typeof []; // 'object'
Масив: Array.isArray(value), а не typeof чи instanceof.
instanceof - чи є в ланцюжку прототипів конструктор: err instanceof TypeError. Пастка - різні realm'и: масив з iframe чи з іншого vm-контексту в Node.js має інший Array, і instanceof Array дає false. Array.isArray таку проблему не має.
Точний «внутрішній» тип:
Object.prototype.toString.call(new Date()); // '[object Date]'
Object.prototype.toString.call(null); // '[object Null]'
Інші корисні перевірки:
Number.isNaN(x),Number.isInteger(x),Number.isFinite(x)- на відміну від глобальнихisNaN/isFinite, не приводять аргумент до числа.value === null- дляnull.value !== null && typeof value === 'object'- «справжній» об'єкт.
Для даних ззовні (відповідь API, localStorage, форма) ручні перевірки швидко стають громіздкими. Тут використовують схеми валідації (Zod, Valibot): вони одночасно перевіряють дані під час виконання й дають TypeScript-тип.
Number точно представляє цілі числа лише до Number.MAX_SAFE_INTEGER = 2^53 - 1 = 9 007 199 254 740 991. Далі сусідні цілі числа вже неможливо розрізнити:
9007199254740993 === 9007199254740992; // true
BigInt - окремий тип цілих чисел довільної довжини:
const big = 9007199254740993n; // суфікс n
big + 2n; // 9007199254740995n
BigInt('123456789012345678901234567890');
Обмеження BigInt: не можна змішувати з Number в арифметиці (1n + 1 - TypeError), Math.* з ним не працює, ділення відкидає дробову частину, а JSON.stringify кидає помилку.
Реальна проблема - ID з бекенду. Twitter/X ID, Snowflake-ідентифікатори, bigint-ключі з бази можуть перевищувати 2^53. JSON.parse перетворює число на Number і тихо спотворює останні цифри:
JSON.parse('{"id": 1234567890123456789}').id; // 1234567890123456800
Запит з таким ID потім шукає не той запис - і помилка проявляється далеко від причини.
Рішення:
- Віддавати великі ID рядками - найпоширеніший підхід (Twitter API повертає і
id, іid_str). У Laravel - каст до рядка в API Resource. JSON.parseз reviver іcontext.source(нові браузери й Node.js) дає доступ до сирого тексту числа, щоб перетворити його наBigInt.- ID - це ідентифікатор, а не число: арифметика над ним не потрібна, тож рядок - природний тип.
Коли об'єкт опиняється там, де потрібен примітив (+obj, obj + '', `${obj}`, obj > 5, obj == 1), рушій викликає перетворення на примітив з «підказкою» (hint), якого типу очікують:
'number'- арифметика й порівняння:+obj,obj * 2,obj > 5;'string'- шаблонні рядки,String(obj), ключі об'єкта;'default'- коли незрозуміло: бінарний+,==.
Алгоритм:
- якщо є метод
obj[Symbol.toPrimitive](hint)- викликається він; - інакше для
'string'пробуютьсяtoString(), потімvalueOf(); - для
'number'і'default'- спершуvalueOf(), потімtoString(); - перший результат-примітив і використовується. Якщо обидва повернули об'єкт -
TypeError.
const price = {
amount: 42,
valueOf() { return this.amount; },
toString() { return `${this.amount} грн`; },
};
price + 1; // 43 (default → valueOf)
price * 2; // 84 (number → valueOf)
`${price}`; // '42 грн' (string → toString)
String(price); // '42 грн'
Повний контроль - Symbol.toPrimitive:
const money = {
[Symbol.toPrimitive](hint) {
if (hint === 'number') return 42;
if (hint === 'string') return '42 грн';
return 'default';
},
};
+money; // 42
`${money}`; // '42 грн'
money + ''; // 'default'
Як це пояснює «дивацтва» JavaScript:
[] + []→'': масиви черезtoString()стають порожніми рядками;[] + {}→'[object Object]';[5] * 2→10:[5].toString()='5', потім число;Date- єдиний вбудований об'єкт, для якого'default'означає рядок:date + 1дає рядок з датою й одиницею, аdate - 1- число (мітку часу мінус 1).
Практична цінність:
- порівняння дат
a < bпрацює черезvalueOf(), що повертає мілісекунди; - власні числові типи (гроші, вектори) можуть поводитися природно в шаблонах, але арифметику через
valueOfкраще не будувати:price1 + price2дасть число й «загубить» валюту. Явні методи (add,format) надійніші; - в об'єктах-значеннях, що потрапляють у шаблони, корисний змістовний
toString()- замість[object Object]у логах і повідомленнях.
JSON.stringify вміє лише те, що є в JSON: об'єкти, масиви, рядки, скінченні числа, true/false, null. Усе інше перетворюється - і часто тихо.
JSON.stringify({
a: undefined, // властивість зникає
b: () => 1, // зникає
c: Symbol('x'), // зникає
d: NaN, // null
e: Infinity, // null
f: new Date(0), // '1970-01-01T00:00:00.000Z'
g: [undefined, () => 1], // [null, null] - у масиві не зникають, а стають null
});
// '{"d":null,"e":null,"f":"1970-01-01T00:00:00.000Z","g":[null,null]}'
Що варто знати:
undefinedу об'єкті - властивість зникає. Для API, що розрізняє «поле не передано» і «поле очистити», треба явно надсилатиnull;NaNіInfinityстаютьnull- сервер отримаєnullзамість помилки в розрахунку;- дати стають рядками ISO в UTC (через метод
toJSON).JSON.parseназад їх не перетворює - на клієнті після розбору це рядки; MapіSetсеріалізуються як{}- дані втрачаються без попередження. Перетворюйте явно:Object.fromEntries(map),[...set];BigIntкидаєTypeError(«Do not know how to serialize a BigInt»). Потрібно перетворювати на рядок вручну;- циклічні посилання (
obj.self = obj, вузли DOM, моделі з двосторонніми зв'язками) кидаютьTypeError.
Керування серіалізацією:
// метод toJSON - об'єкт сам вирішує, як виглядати в JSON
class Money {
constructor(amount, currency) { Object.assign(this, { amount, currency }); }
toJSON() { return { amount: String(this.amount), currency: this.currency }; }
}
// replacer-функція: перетворити BigInt, приховати пароль
JSON.stringify(data, (key, value) => {
if (typeof value === 'bigint') return value.toString();
if (key === 'password') return undefined;
return value;
});
// replacer-масив - білий список полів
JSON.stringify(user, ['id', 'name']);
// відступи для читабельності
JSON.stringify(data, null, 2);
Зворотний бік - JSON.parse з reviver:
JSON.parse(text, (key, value) =>
typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value) ? new Date(value) : value,
);
Наслідок для «глибокого копіювання» через JSON.parse(JSON.stringify(x)): воно губить undefined, функції, перетворює дати на рядки, Map на {}, падає на циклах. Для копіювання є structuredClone(), що коректно обробляє дати, Map, Set і цикли.