Senior: питання на співбесіді з теми «Типи й приведення»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Універсального способу немає - для різних типів різні інструменти.
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 і цикли.