Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді: Типи й приведення

Питання з реальних співбесід з відповідями: 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).

Докладніше в документації: parseInt()

Усі числа в 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').

Докладніше в документації: Number

У логічному контексті (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.

Докладніше в документації: Falsy

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 при відправці на сервер.

Докладніше в документації: Number.isNaN()

Об'єкт 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).

Докладніше в документації: Date

Універсального способу немає - для різних типів різні інструменти.

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-тип.

Докладніше в документації: typeof

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 - це ідентифікатор, а не число: арифметика над ним не потрібна, тож рядок - природний тип.

Докладніше в документації: BigInt

Коли об'єкт опиняється там, де потрібен примітив (+obj, obj + '', `${obj}`, obj > 5, obj == 1), рушій викликає перетворення на примітив з «підказкою» (hint), якого типу очікують:

  • 'number' - арифметика й порівняння: +obj, obj * 2, obj > 5;
  • 'string' - шаблонні рядки, String(obj), ключі об'єкта;
  • 'default' - коли незрозуміло: бінарний +, ==.

Алгоритм:

  1. якщо є метод obj[Symbol.toPrimitive](hint) - викликається він;
  2. інакше для 'string' пробуються toString(), потім valueOf();
  3. для 'number' і 'default' - спершу valueOf(), потім toString();
  4. перший результат-примітив і використовується. Якщо обидва повернули об'єкт - 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] у логах і повідомленнях.

Докладніше в документації: Symbol.toPrimitive

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 і цикли.

Докладніше в документації: JSON.stringify()