08 · JavaScript/TypeScript для AQA
Live coding для AQA: типові задачі та підходи
Зміст
Live coding — це формат, де тебе просять розвʼязати невелику задачу прямо в редакторі чи на дошці, вголос пояснюючи, що робиш. Для AQA-вакансій такі задачі приземлені — не «напиши алгоритм Дейкстри», а розверни рядок, знайди дублікати в масиві, згрупуй , зроби retry для -запиту. Задачі прості, але саме на них валяться кандидати — бо хвилюються, забувають про й пишуть код, який на першому ж порожньому масиві падає.
Головна пастка live coding у тому, що оцінюють не сам розвʼязок, а те, як ти мислиш. Той самий FizzBuzz можна зробити за 30 секунд і отримати «ну ок», а можна за три хвилини — уточнивши вимоги, проговоривши межі й перевіривши результат — і отримати «беремо». Ця глава показує канонічні задачі й, головне, патерн міркування, який перетворює написання коду на демонстрацію тестувальницького мислення. Мова прикладів — TypeScript, бо саме її очікують на JS/TS-стеку; синтаксис методів масивів і рядків розібрано в главах Обʼєкти, масиви та методи роботи з даними і Рядки, дати й регулярні вирази.
Що насправді оцінюють
Інтервʼюер дивиться не тільки на робочий код. За той самий час він зчитує чотири речі, і три з них — не про алгоритми:
- Чи уточнюєш вимоги. Задачу формулюють навмисно неповно. «Розверни рядок» — а що з емодзі? З порожнім рядком? Реверс за символами чи за словами? Кандидат, який одразу питає, показує зрілість; кандидат, який кинувся кодити, показує, що так само кинеться писати автотест без розуміння очікуваного результату.
- Чи бачиш edge cases. Це прямий твоєї основної навички. Порожнє,
null, дублікати, одиничний елемент, дуже великий вхід, unicode — те саме, що ти щодня закладаєш у тест-дизайн ( й живуть у розділі «Тест-дизайн»). - Чи пояснюєш вголос. Мовчазний геній, який за пʼять хвилин видає ідеальний код без єдиного слова, програє тому, хто думає вголос. Автоматизатор постійно пояснює рішення на код-рев'ю — інтервʼю це репетирує.
- Чи перевіряєш себе. Дописав — і одразу прогнав подумки на кількох входах, зокрема на edge cases. Це рефлекс тестувальника, і його видно неозброєним оком.
Тому далі кожну задачу подано не як «ось відповідь», а разом із межами, які варто проговорити.
Патерн міркування: вимоги → edge cases → рішення → перевірка
Це кістяк, на який нанизується будь-яка задача. Він же — те, чим QA відрізняється від розробника на тому самому завданні: розробник біжить до робочого коду, тестувальник спершу окреслює простір входів.
Проговори це буквально такими словами на співбесіді:
- Вимоги. «Уточню: вхід — це завжди рядок? Реверс посимвольний? Регістр важливий?» Дві-три хвилини тут економлять переписування потім.
- Edge cases. Назви їх до коду: порожній вхід, один елемент, дублікати, від'ємні числа, дуже великий масив,
undefinedсеред даних. Навіть якщо не оброблятимеш усі — покажи, що бачиш. - Рішення. Спершу найпростіше, що працює й читається. Не оптимізуй передчасно; якщо спитають про складність — тоді й обговориш.
- Перевірка. Прогони код подумки на і на кількох краях. Вголос: «порожній рядок → повертає порожній, ок; один символ → сам себе, ок».
Далі — задачі, кожна крізь цю призму.
Рядки: реверс, паліндром, частотний словник
Реверс рядка. Класика, у якій ховається справжнє питання про unicode.
function reverse(input: string): string {
return [...input].reverse().join('');
}
Чому [...input], а не input.split('')? Спред перебирає рядок за код-поінтами (Unicode code points), тож емодзі й символи з сурогатних пар не розваляться навпіл. split('') ріже за 16-бітними одиницями й на "👍" дасть кашу. Проговорити цю різницю — і задача-«розминка» вже показала, що ти знаєш, де в рядках болить. Edge cases: порожній рядок (повертає порожній), один символ, рядок із комбінованими діакритиками (тут навіть код-поінти не рятують — але про це чесно сказати як про ).
Паліндром. Питання-пастка — не «як порівняти з реверсом», а «що вважати паліндромом». Майже завжди мають на увазі: ігнорувати регістр, пробіли й пунктуацію.
function isPalindrome(input: string): boolean {
const normalized = input.toLowerCase().replace(/[^a-z0-9]/gi, '');
let left = 0;
let right = normalized.length - 1;
while (left < right) {
if (normalized[left] !== normalized[right]) return false;
left++;
right--;
}
return true;
}
Два вказівники замість normalized === [...normalized].reverse().join('') — не заради швидкості (обидва читаються нормально), а щоб показати, що ти можеш і без «магії» методів. Але якщо напишеш через реверс і поясниш — теж валідно. Edge cases: порожній рядок (зазвичай вважають паліндромом), один символ, "A man, a plan, a canal: Panama" — саме на цьому вході видно, чи проговорив ти нормалізацію.
Частотний словник. Порахувати, скільки разів кожен елемент зустрічається — база для купи реальних перевірок (розподіл статусів у відповіді API, дублікати в експорті).
function frequency(items: string[]): Map<string, number> {
const counts = new Map<string, number>();
for (const item of items) {
counts.set(item, (counts.get(item) ?? 0) + 1);
}
return counts;
}
Map кращий за звичайний обʼєкт: ключі будь-якого типу, немає колізій із успадкованими властивостями штибу constructor, і порядок вставки збережено. Оператор ?? дає нуль для ще не баченого ключа. Edge cases: порожній масив (порожня мапа), усі елементи однакові, різний регістр (уточни, чи "A" і "a" — те саме).
Масиви: дублікати, унікальні, перетин, сортування обʼєктів
Унікальні значення — однорядковий ідіом, який мусиш видавати не думаючи:
const unique = [...new Set(items)];
Знайти дублікати — трохи хитріше, бо треба саме ті, що повторюються:
function findDuplicates(items: number[]): number[] {
const seen = new Set<number>();
const dupes = new Set<number>();
for (const item of items) {
if (seen.has(item)) dupes.add(item);
else seen.add(item);
}
return [...dupes];
}
Два Set — щоб не отримати сам дублікат кількаразово в результаті. Тут добре проговорити складність: один прохід, O(n), замість наївного подвійного циклу O(n²).
Перетин двох масивів — спільні елементи. Ключова оптимізація: другий масив у Set, щоб перевірка була константною.
function intersection(a: number[], b: number[]): number[] {
const setB = new Set(b);
return [...new Set(a)].filter((x) => setB.has(x));
}
Зовнішній new Set(a) прибирає дублікати з результату — уточни в інтервʼюера, чи це бажано. Edge cases для всіх трьох: порожній масив, повний збіг, жодного спільного елемента, дублікати на вході.
Сортування обʼєктів — тут ховається найвідоміша пастка JavaScript. Метод sort без компаратора сортує як рядки, тож [10, 9, 1].sort() дасть [1, 10, 9]. І він мутує вихідний масив.
const users = [
{ name: 'Ada', age: 36 },
{ name: 'Alan', age: 41 },
{ name: 'Grace', age: 36 },
];
// за віком зростанням, копія без мутації оригіналу
const byAge = [...users].sort((a, b) => a.age - b.age);
Компаратор повертає число: відʼємне — a перед b, додатне — навпаки, нуль — порядок збережено. Для чисел — різниця, для рядків — localeCompare (щоб коректно з українськими літерами). Із ES2019 sort гарантовано стабільний, тож елементи з однаковим age збережуть відносний порядок — на це можна спертися. Edge cases: порожній масив, один елемент, однакові ключі (перевірка стабільності), змішані типи.
Масив обʼєктів як тестові дані: фільтрація, групування, агрегація
Це найближча до реальної роботи AQA задача: тобі дають масив обʼєктів (відповідь API, вибірку з БД) і просять щось із ним зробити. Тут метод reduce — головний інструмент, і вміння його пояснити цінується.
Фільтрація тривіальна (filter), тож усе цікаве починається на групуванні.
Групування — розкласти елементи по кошиках за ключем:
interface TestRun {
suite: string;
status: 'passed' | 'failed' | 'skipped';
durationMs: number;
}
function groupByStatus(runs: TestRun[]): Map<string, TestRun[]> {
return runs.reduce((groups, run) => {
const bucket = groups.get(run.status) ?? [];
bucket.push(run);
groups.set(run.status, bucket);
return groups;
}, new Map<string, TestRun[]>());
}
Сучасний (Node 21+) уміє Object.groupBy(runs, (r) => r.status) — але не в кожному середовищі воно є, тож на співбесіді безпечніше показати варіант через reduce, а про Object.groupBy згадати як новіший цукор. Це саме та обережність із доступністю API, яку від тебе й чекають.
Агрегація — порахувати підсумок по групі: кількість, суму, середнє.
function averageDuration(runs: TestRun[]): number {
if (runs.length === 0) return 0; // ділення на нуль!
const total = runs.reduce((sum, run) => sum + run.durationMs, 0);
return total / runs.length;
}
Порожній масив тут — не формальність, а справжня діра: без перевірки отримаєш NaN від ділення на нуль. Назвати цей edge case до того, як інтервʼюер тицьне в нього, — половина успіху. Комбінована задача «згрупуй прогони за сюїтою і порахуй середню тривалість кожної» перевіряє все одразу; розкладай її на групування, а потім агрегацію кожної групи — не намагайся зробити в один прохід, поки не запрацює простий варіант.
Async-задачі: retry, послідовність, ліміт паралельності
Асинхронні задачі відсіюють тих, хто плутається в await. Механіку розібрано в главі Асинхронність: Promises, async/await і паралельність; тут — типові live-coding формулювання. Усім трьом потрібен хелпер :
const delay = (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms));
Retry із затримкою — прямий болючий кейс автоматизатора: повторити нестабільну операцію кілька разів, перш ніж здатися.
async function retry<T>(
action: () => Promise<T>,
attempts = 3,
delayMs = 500,
): Promise<T> {
let lastError: unknown;
for (let i = 0; i < attempts; i++) {
try {
return await action();
} catch (error) {
lastError = error;
if (i < attempts - 1) await delay(delayMs);
}
}
throw lastError;
}
Ключові моменти для проговорювання: не спати після останньої спроби (нема сенсу); зберегти й прокинути останню помилку, а не проковтнути її; <T>, щоб типізація не губилась. Логічне продовження — експоненційний : delayMs * 2 ** i. Тут же доречно згадати тестувальницьку мудрість: retry маскує , а не лікує його (таксономія — тема розділу «Автоматизація: стратегія»).
Послідовні запити — виконати по черзі, коли кожен наступний залежить від попереднього. Головна пастка — forEach не чекає await всередині:
// НЕ спрацює: forEach ігнорує проміси, усе стартує разом
ids.forEach(async (id) => { await fetchUser(id); });
// правильно: for...of дає справжню послідовність
async function fetchSequentially(ids: number[]): Promise<User[]> {
const users: User[] = [];
for (const id of ids) {
users.push(await fetchUser(id));
}
return users;
}
Ліміт паралельності — найскладніша з трійки. Promise.all запускає все одразу; якщо треба «не більше N одночасно» (щоб не завалити сервер), потрібен пул:
async function withLimit<T>(
tasks: (() => Promise<T>)[],
limit: number,
): Promise<T[]> {
const results: T[] = new Array(tasks.length);
let cursor = 0;
async function worker(): Promise<void> {
while (cursor < tasks.length) {
const index = cursor++;
results[index] = await tasks[index]();
}
}
const pool = Array.from({ length: Math.min(limit, tasks.length) }, worker);
await Promise.all(pool);
return results;
}
Тут важлива ідея: tasks — це масив функцій, що повертають проміси, а не готових промісів; інакше вони стартують до того, як пул почне їх притримувати. limit розбирають завдання зі спільного курсора. Порядок результатів збережено через індекс. Edge cases: порожній список, limit більший за кількість задач, задача, що падає (уточни — чи зупиняти весь пул). Якщо не встигаєш дописати пул — чесно скажи ідею вголос; часто цього достатньо.
FizzBuzz як задача на дизайн
FizzBuzz («Fizz» на кратних 3, «Buzz» на кратних 5, «FizzBuzz» на кратних 15, інакше число») сам по собі тривіальний. Але на ньому добре видно розширюваність: «а тепер додай правило Bazz на кратних 7». Наївний варіант із каскадом if доведеться переписувати; тому цінують рішення, де правила — це дані.
class FizzBuzz {
constructor(private rules: [number, string][] = [[3, 'Fizz'], [5, 'Buzz']]) {}
convert(n: number): string {
const output = this.rules
.filter(([divisor]) => n % divisor === 0)
.map(([, word]) => word)
.join('');
return output || String(n);
}
}
const fb = new FizzBuzz([[3, 'Fizz'], [5, 'Buzz'], [7, 'Bazz']]);
// fb.convert(15) → 'FizzBuzz'; fb.convert(21) → 'FizzBazz'; fb.convert(1) → '1'
Тут «FizzBuzz» на 15 виходить автоматично — рядки склеюються, бо 15 кратне і 3, і 5. Нове правило — один елемент у масиві, жодного if переписувати. Це демонструє два принципи, які цінує ревʼюер тестового коду: конфігурація замість гілок і одна причина для зміни. Синтаксис класів — у главі this, класи та ООП у тестовому коді. Edge cases: 0 (кратне всьому — поверне склейку всіх слів, уточни, чи це очікувано), відʼємні числа, дуже велике n.
Типові помилки
- Виглядає як «зрозумів задачу», а насправді не уточнив очікуваний результат. Кинувся кодити реверс — і зробив посимвольний, коли просили за словами. Дві хвилини питань дешевші за переписування під тиском.
- Виглядає як робочий код, а насправді падає на порожньому вході.
averageбез перевірки довжини даєNaN,[].reduceбез початкового значення кидає виняток. Порожнє — edge case номер один, завжди називай його першим. - Виглядає як сортування, а насправді лексикографічне.
[2, 10, 1].sort()→[1, 10, 2]. Забутий компаратор нічого не ламає видимо — він тихо дає неправильний порядок. - Виглядає як async-цикл, а насправді все стартувало разом.
awaitусерединіforEachігнорується; проміси не чекаються, а помилки в них стають unhandled rejection. Для послідовності —for...of. - Виглядає як паралельність із лімітом, а насправді
Promise.allнад готовими промісами. Якщо передати вже створені проміси, вони стартують негайно, і жоден «ліміт» їх не притримає. Ліміт працює лише над функціями, що відкладають старт. - Виглядає як мовчазна зосередженість, а насправді втрачені бали. Не пояснювати хід думки — програшна стратегія навіть із правильним кодом.
Підсумок
- Live coding для AQA оцінює мислення, а не алгоритми: уточнення вимог, бачення edge cases, пояснення вголос і самоперевірка важать більше за швидкість.
- Патерн вимоги → edge cases → рішення → перевірка — це той самий тест-дизайн, лише застосований до власного коду; проговорюй його явно.
- Порожній вхід, один елемент, дублікати, unicode й
null— універсальний список меж; називай їх до написання коду, а не після тику інтервʼюера. - Знай напамʼять ідіоми:
[...new Set()]для унікальних,Setдля перетину/дублікатів заO(n),reduceдля групування й агрегації, компаратор дляsort. - В async уникай
awaitуforEach, розумій різницю послідовногоfor...ofіPromise.all, а ліміт паралельності будуй над функціями, не над промісами.
Можливі питання
- «Розверни рядок» — інтервʼюер чекає, що спитаєш про unicode й порожній вхід; сам однорядковик менш важливий за ці питання.
- «Знайди дублікати / перетин масивів» — дивляться, чи згадаєш
Setі чи оціниш складність; наївнийO(n²)покаже прогалину. - «Згрупуй ці дані й порахуй підсумок» — перевірка
reduceі того, чи обробиш порожню групу безNaN; проксі реальної роботи з відповіддю API. - «Зроби retry для нестабільного запиту» — чекають коректну затримку (не після останньої спроби), прокидання помилки і зауваження, що retry маскує флак.
- «Обмеж кількість одночасних запитів» — найскладніше; навіть якщо не допишеш пул, цінують правильно сформульовану ідею з функціями-задачами.
- «Ось FizzBuzz — тепер додай правило» — перевіряють, чи закладеш розширюваність через дані, а не каскад
if.
На що дивиться інтервʼюер понад код: чи ти уточнюєш перед тим, як писати; чи проговорюєш edge cases; чи перевіряєш себе на межах наприкінці. Це рівно ті рефлекси, які потім підуть у тест-дизайн і код-рев'ю. Якщо застряг — думай уголос: мовчазний глухий кут гірший за озвучену гіпотезу.
Джерела
- ISTQB Glossary — означення
edge case— нетиповий вхід усередині меж, який відрізняють від граничного значення.
Патерн міркування: вимоги → edge cases → рішення → перевірка
- ISTQB Glossary — означення
edge case— нетиповий вхід усередині меж, який відрізняють від граничного значення.
Рядки: реверс, паліндром, частотний словник
- MDN — String — рядок як послідовність UTF-16:
split("")розриває сурогатні пари, а[Symbol.iterator]іде по кодових точках;localeCompare()сортує з урахуванням локалі. - MDN — String.prototype.normalize() — чотири форми нормалізації Unicode — межа, за якою навіть обхід по кодових точках не рятує складені символи.
- MDN — Map —
Mapгарантує порядок вставки, приймає ключ будь-якого типу й не має «дефолтних» ключів прототипу. - MDN — Nullish coalescing operator (??) —
??підставляє значення лише дляnullіundefined— саме те, що треба для ще не баченого ключа.
Масиви: дублікати, унікальні, перетин, сортування обʼєктів
- MDN — Set —
Setтримає унікальні значення, аhas()у середньому швидший заArray.prototype.includes(). - MDN — Array.prototype.sort() —
sort()мутує масив, без компаратора порівнює рядкові подання UTF-16, а стабільність нормативно гарантована з ECMAScript 2019. - MDN — Array — три класи методів масиву й контракт ітеративних: , ,
sort()як окремий випадок. - MDN — String — рядок як послідовність кодових одиниць UTF-16:
split("")розриває сурогатні пари, а[Symbol.iterator]іде по кодових точках;localeCompare()сортує з урахуванням локалі.
Масив обʼєктів як тестові дані: фільтрація, групування, агрегація
- MDN — Array — три класи методів масиву й контракт ітеративних: колбек, поверхнева копія,
sort()як окремий випадок. - MDN — Array.prototype.reduce() — дві форми
reduce()іTypeErrorна порожньому масиві без початкового значення. - MDN — Map —
Mapгарантує порядок вставки, приймає ключ будь-якого типу й не має «дефолтних» ключів прототипу. - MDN — Nullish coalescing operator (??) —
??підставляє значення лише дляnullіundefined— саме те, що треба для ще не баченого ключа.
Async-задачі: retry, послідовність, ліміт паралельності
- MDN — Using promises (JavaScript Guide) — чотири інструменти композиції запускають проміси одночасно; послідовність доводиться будувати окремо.
- MDN — Promise —
Promise.allі сусідні статичні методи: коли виконуються, коли відхиляються, що віддають. - MDN — async function —
asyncзавжди повертає проміс, а межа синхронного проходить по першомуawait. - typescript-eslint — no-floating-promises — правило вимагає щось зробити з кожним промісом — включно з масивом промісів, який треба віддати в
Promise.all(). - Google Testing Blog — Flaky Tests at Google and How We Mitigate Them — знижує хибні спрацювання, але заохочує ігнорувати флак — тобто маскує його, а не лікує.
- MDN — Classes (JavaScript reference) — клас як шаблон, що інкапсулює дані разом із кодом, і розкладка його елементів.
- MDN — Array — три класи методів масиву й контракт ітеративних: колбек, поверхнева копія,
sort()як окремий випадок. - MDN — Falsy (glossary) — порожній рядок — , і саме на цьому тримається ідіома
output || String(n).
- MDN — Array.prototype.reduce() — дві форми
reduce()іTypeErrorна порожньому масиві без початкового значення. - MDN — Array.prototype.sort() —
sort()мутує масив, без компаратора порівнює рядкові подання UTF-16, а стабільність нормативно гарантована з ECMAScript 2019. - MDN — Array — три класи методів масиву й контракт ітеративних: колбек, поверхнева копія,
sort()як окремий випадок. - MDN — Using promises (JavaScript Guide) — чотири інструменти композиції запускають проміси одночасно; послідовність доводиться будувати окремо.
- typescript-eslint — no-floating-promises — правило вимагає щось зробити з кожним промісом — включно з масивом промісів, який треба віддати в
Promise.all().
- MDN — Set —
Setтримає унікальні значення, аhas()у середньому швидший заArray.prototype.includes(). - MDN — Array.prototype.sort() —
sort()мутує масив, без компаратора порівнює рядкові подання UTF-16, а стабільність нормативно гарантована з ECMAScript 2019. - MDN — Array.prototype.reduce() — дві форми
reduce()іTypeErrorна порожньому масиві без початкового значення. - MDN — Using promises (JavaScript Guide) — чотири інструменти композиції запускають проміси одночасно; послідовність доводиться будувати окремо.
- MDN — String — рядок як послідовність кодових одиниць UTF-16:
split("")розриває сурогатні пари, а[Symbol.iterator]іде по кодових точках;localeCompare()сортує з урахуванням локалі.
- MDN — String — рядок як послідовність кодових одиниць UTF-16:
split("")розриває сурогатні пари, а[Symbol.iterator]іде по кодових точках;localeCompare()сортує з урахуванням локалі. - MDN — Set —
Setтримає унікальні значення, аhas()у середньому швидший заArray.prototype.includes(). - MDN — Array.prototype.reduce() — дві форми
reduce()іTypeErrorна порожньому масиві без початкового значення. - MDN — Using promises (JavaScript Guide) — чотири інструменти композиції запускають проміси одночасно; послідовність доводиться будувати окремо.
- MDN — Classes (JavaScript reference) — клас як шаблон, що інкапсулює дані разом із кодом, і розкладка його елементів.
Що таке live coding на співбесіді AQA і що на ньому насправді перевіряють?
Це формат, де тобі дають невелику задачу й просять розвʼязати її прямо в редакторі, коментуючи кожен крок. Для автоматизатора такі задачі побутові — не олімпіадний алгоритм, а перевернути рядок, вимести дублікати з масиву, розкласти по групах. Але дивляться не на сам код: інтервʼюер зчитує, чи ти уточнюєш умову, чи бачиш , чи міркуєш уголос і чи перевіряєш себе наприкінці. Це ті самі рефлекси, що потім працюють у тест-дизайні та на код-рев'ю, тому задача-«розминка» насправді є твоєї основної навички. Кандидат, який мовчки видав ідеальний однорядковик, часто програє тому, хто три хвилини розкладав задачу вголос.
Тобі дали задачу «обробити масив». Що зробиш перед тим, як писати перший рядок?
Спершу окреслю простір входів і виходів, а не кинуся кодити — саме цим QA і відрізняється від розробника на тому самому завданні. Уточню тип і формат входу, чого чекають на виході, і чи можливі особливі значення. Потім уголос назву edge cases: порожній масив, один елемент, дублікати, null або undefined серед даних, дуже великий вхід. Навіть якщо не оброблятиму їх усі, показую, що бачу межі — це прямий сигнал зрілості. Тільки після цього беруся за найпростіше робоче рішення, а наприкінці проганяю його подумки на і на кількох краях.
Розверни рядок. Що б ти уточнив і де тут пастка?
Спершу спитаю, чи реверс посимвольний чи за словами, і що робити з порожнім входом — умову навмисно дають неповною. Базове рішення в TypeScript — розкласти рядок у масив, перевернути й склеїти назад. Головний нюанс — unicode: якщо різати рядок через split(''), він поділиться за 16-бітними одиницями, і емодзі чи символ із сурогатної пари розваляться навпіл. Тому надійніше перебирати через спред [...input], який іде за код-поінтами й не ламає такі символи. Чесно варто додати межу власних знань: комбіновані діакритики навіть код-поінти можуть рознести, і тут потрібен уже сегментатор графем. Edge cases — порожній рядок повертає порожній, один символ дає сам себе.
Як перевірити, що рядок — паліндром, і в чому головна пастка задачі?
Пастка не в порівнянні з реверсом, а в тому, що взагалі вважати паліндромом. Майже завжди мають на увазі ігнорувати регістр, пробіли й пунктуацію, тож перший крок — нормалізувати рядок: привести до нижнього регістру й лишити тільки букви та цифри. Далі можна порівняти з його ж реверсом, але охайніше — двома вказівниками з країв до центру: біжимо, поки символи збігаються, і на першій розбіжності повертаємо false. Двовказівниковий варіант показує, що вмієш і без «магії» методів, хоча реверс із поясненням теж валідний. Класичний перевірочний вхід — фраза на кшталт "A man, a plan, a canal: Panama": саме на ньому видно, чи ти проговорив нормалізацію. Порожній рядок і один символ зазвичай вважають паліндромами.
Порахуй, скільки разів кожен елемент зустрічається. Чому для лічильника краще Map, а не обʼєкт?
Це частотний словник — база для купи реальних перевірок штибу розподілу статусів у відповіді API чи пошуку дублікатів в експорті. Проходжу масив і для кожного елемента збільшую лічильник; оператор ?? дає нуль для ще не баченого ключа. Map тут кращий за звичайний обʼєкт із кількох причин: ключем може бути значення будь-якого типу, немає колізій зі службовими іменами штибу constructor чи toString, і порядок вставки збережено. Обʼєкт же має прототип, тож ключ, що збігається з успадкованою властивістю, може дати сюрприз. Edge cases: порожній масив дає порожню мапу, а різний регістр варто уточнити окремо — чи "A" і "a" рахуються як одне.
Дай унікальні значення й окремо — тільки дублікати. Чим ці задачі різняться?
Унікальні значення — це ідіома, яку треба видавати не думаючи: обгорнути масив у Set і розкласти назад через спред. Set за визначенням тримає лише неповторні значення, тож дублікати відсіюються самі. А от знайти саме ті елементи, що повторюються, трохи хитріше: тримаю два Set — «бачені» й «дублікати», і коли елемент трапляється вдруге, кладу його в другий. Два набори потрібні, щоб один елемент, який повторюється тричі, не потрапив у результат кілька разів. Тут доречно проговорити складність: один прохід дає O(n) проти наївного подвійного циклу з O(n²). Згадка про Set і про оцінку складності — саме те, чого чекає інтервʼюер.
Знайди спільні елементи двох масивів. Як зробити це ефективно?
Наївне рішення — для кожного елемента першого масиву пробігати другий — дає квадратичну складність. Оптимізація в тому, щоб покласти другий масив у Set: тоді перевірка «чи є елемент» стає константною, і весь перетин рахується за лінійний час. Зазвичай ще прибирають дублікати з результату, обгорнувши перший масив у Set перед фільтрацією, — але це варто уточнити в інтервʼюера, бо іноді дублікати в перетині потрібні. Проговори припущення вголос: чи вважати [1, 1, 2] та [1, 2] спільним [1, 2] чи [1, 1, 2]. Edge cases для таких задач однакові: порожній масив, повний збіг, жодного спільного елемента, дублікати на вході.
Відсортуй масив обʼєктів за полем. Які дві класичні пастки JavaScript тут чигають?
Перша пастка — метод sort без компаратора сортує елементи як рядки, тож [10, 9, 1].sort() дасть [1, 10, 9], а не те, що очікуєш. Тому для чисел завжди передаю компаратор — різницю a.age - b.age, а для рядків localeCompare, щоб коректно врахувати українські літери. Друга пастка — sort мутує вихідний масив, а не повертає копію, тому спершу роблю через спред і сортую вже її, аби не зіпсувати оригінал. Компаратор працює за правилом: відʼємне число ставить a перед b, додатне — навпаки, нуль лишає порядок. Забутий компаратор нічого не ламає видимо — він тихо дає неправильний порядок, і назвати обидві пастки вголос коштує дорожче за сам код.
Чи гарантовано стабільне сортування в JavaScript і чому це важливо?
Так, із ES2019 специфікація вимагає, щоб sort був стабільним: елементи, які компаратор вважає рівними, зберігають свій відносний порядок. На практиці це означає, що якщо відсортувати прогони за тривалістю, а потім за статусом, то всередині кожного статусу давніший порядок за тривалістю не зруйнується — на це можна спертися при багаторівневому сортуванні. До ES2019 поведінка залежала від , тож старі приклади цього не гарантували. Для перевірки стабільності хороший edge case — кілька елементів з однаковим ключем сортування: якщо їхній порядок зберігся, компаратор і стабільність працюють як треба. Це той нюанс, що відрізняє поверхневу відповідь від глибокої.
Згрупуй масив обʼєктів за ключем. Який інструмент і що згадати про доступність API?
Групування — це розкласти елементи по кошиках за спільним ключем, і головний інструмент тут reduce з Map як акумулятором: для кожного елемента дістаю відповідний кошик (або створюю порожній через ??), додаю елемент і повертаю мапу далі. Уміння пояснити крок reduce уголос цінується окремо, бо багато хто його боїться. Варто згадати й новіший синтаксичний цукор Object.groupBy, який робить те саме одним викликом, — але одразу додати, що він доступний не в кожному середовищі (потрібен свіжий JS-рушій), тож на співбесіді безпечніше показати варіант через reduce. Саме ця обережність із сумісністю API — те, чого від автоматизатора й чекають. Edge case — порожній вхід, який має дати порожню структуру без падіння.
Порахуй середню тривалість по групі прогонів. Який edge case тут номер один?
Порожній масив — і це не формальність, а справжня діра: якщо просто поділити суму на довжину, на порожньому вході отримаєш NaN від ділення на нуль. Тому першим рядком ставлю перевірку довжини й повертаю нуль (або кидаю зрозумілу помилку — уточни, що очікують) до будь-яких обчислень. Далі сумую тривалості через reduce з початковим значенням і ділю на кількість. Назвати цей edge case до того, як інтервʼюер тицьне в нього пальцем, — половина успіху. Комбіновану задачу «згрупуй за сюїтою і порахуй середнє кожної групи» варто розкладати на два кроки: спершу групування, потім агрегація кожної групи, а не намагатися зробити все в один прохід, поки простий варіант не запрацює.
Напиши retry для нестабільного запиту. Які моменти обовʼязково проговорити?
Retry — прямий болючий кейс автоматизатора: повторити операцію кілька разів, перш ніж визнати провал. Загальна форма — цикл на задану кількість спроб, де кожну спробу обгортаю в try/catch; при успіху одразу повертаю результат, при помилці — зберігаю її й чекаю перед наступним колом. Ключових деталей три: не спати після останньої спроби, бо чекати вже нема сенсу; зберегти й прокинути останню помилку, а не мовчки проковтнути її; і зробити функцію , щоб тип результату не губився. Логічне продовження — експоненційний , коли множиться з кожним колом. І доречно додати тестувальницьку думку: retry маскує , а не лікує його, тож у продакшн-тестах ним не варто затуляти справжню нестабільність.
Чому await усередині forEach не працює і як виконати запити послідовно?
forEach не вміє чекати на : він викликає передану для кожного елемента, але не зупиняється на await усередині неї, тож усі ітерації стартують практично одночасно, а сам forEach завершується до того, як бодай один проміс виконався. Гірше, що помилки в цих промісах нікому не адресовані й перетворюються на unhandled rejection. Якщо потрібна справжня послідовність — коли кожен наступний крок залежить від попереднього, — використовую цикл for...of: він поважає await і виконує ітерації по черзі. Пастка підступна саме тим, що код виглядає правильним і навіть іноді «майже працює». Якщо ж послідовність не потрібна, а треба лише дочекатися всіх, тоді підходить Promise.all над масивом промісів.
Обмеж кількість одночасних запитів до N. Чому важливо будувати ліміт над функціями, а не над промісами?
Promise.all запускає все відразу, тож коли треба «не більше N водночас» (щоб не завалити сервер), потрібен пул . Критична ідея: на вхід має йти масив функцій, що повертають проміси, а не масив уже створених промісів. Проміс починає виконуватися в момент створення, тому якщо передати готові проміси, вони всі стартують негайно, і жоден «ліміт» їх уже не притримає — обмеження працює лише над відкладеним стартом. Сам пул — це N воркерів, які розбирають задачі зі спільного лічильника-курсора, а результати кладуться за індексом, щоб зберегти порядок. Edge cases: порожній список, limit більший за кількість задач, і задача, що падає, — тут варто уточнити, чи зупиняти весь пул. Навіть якщо не встигаєш дописати код, чітко сформульована ідея з функціями-задачами часто зараховується.
Тобі дали FizzBuzz і кажуть «а тепер додай нове правило». Що перевіряють і як відповісти сильно?
Сам FizzBuzz тривіальний, тож коли просять додати правило (скажімо, слово на кратних 7), перевіряють розширюваність, а не вміння написати каскад if. Наївний варіант із гілками доведеться переписувати під кожне нове правило, і саме це вважають слабким рішенням. Сильна відповідь виносить правила в дані — масив пар «дільник → слово», — а сама функція перебирає їх, збирає слова для всіх дільників, на які число ділиться, і склеює; якщо жодне не підійшло, повертає число. Тоді «FizzBuzz» на 15 виходить автоматично, бо склеюються обидва слова, а нове правило — це просто ще один елемент масиву без жодного нового if. За таким рішенням ревʼюер тестового коду читає дві важливі речі: поведінку задають конфігурацією, а не розгалуженнями, і код має єдину причину змінюватися. Edge cases — нуль, який кратний усьому, та відʼємні числа.
Ти застряг посеред задачі й не памʼятаєш синтаксис. Що робити?
Найгірше — замовкнути й мовчки битися об стіну: інтервʼюер бачить глухий кут, але не бачить думки, тож не може ані підказати, ані зарахувати хід міркувань. Правильна стратегія — думати вголос: озвучити, що саме гальмує, яку ідею намагаюся реалізувати й де саме забув деталь. Часто цього достатньо, щоб інтервʼюер підкинув потрібний метод або підтвердив напрям, — а озвучена гіпотеза завжди краща за озвучене мовчання. Якщо забув конкретний метод, можна назвати його роль («потрібно щось, що прибирає дублікати — це Set») і рухатися далі, домовившись уточнити синтаксис потім. Здатність не панікувати й вести діалог у складний момент оцінюють не менше за готовий код, бо на реальній роботі застрягання — теж норма.
Три розбори, які показують не «правильну відповідь», а хід до неї: як провести задачу крізь патерн вимоги → → рішення → перевірка, як зробити async-задачу робочою в реальному Playwright-контексті і як обрати правильний спосіб паралельності за .
Кейс 1. Проводимо задачу крізь патерн: групування + середнє
Умова, яку почуєш майже дослівно: «Ось масив прогонів тестів, згрупуй їх за сюїтою і порахуй середню тривалість кожної сюїти». Спокуса — одразу почати писати reduce. Сильний кандидат натомість спершу вголос проходить чотири кроки.
Крок 1. Вимоги. «Уточню: тривалість завжди число в мілісекундах? Сюїта — це рядок, чи може бути порожньою? На виході потрібна мапа сюїта → середнє чи масив обʼєктів? Середнє округлювати?» Дві-три хвилини тут рятують від переписування.
Крок 2. Edge cases — називаю до коду. Порожній вхідний масив; з єдиним прогоном; сюїта, де всі тривалості нульові; поле durationMs, якого раптом немає в елементі. Ключовий серед них — порожня група, бо середнє від неї дає NaN.
Крок 3. Рішення — найпростіше, що працює. Розкладаю на два зрозумілі кроки замість одного розумного проходу: спершу групую, потім рахую середнє кожної групи.
interface TestRun {
suite: string;
status: 'passed' | 'failed' | 'skipped';
durationMs: number;
}
function groupBySuite(runs: TestRun[]): Map<string, TestRun[]> {
return runs.reduce((groups, run) => {
const bucket = groups.get(run.suite) ?? [];
bucket.push(run);
groups.set(run.suite, bucket);
return groups;
}, new Map<string, TestRun[]>());
}
function averageDurationBySuite(runs: TestRun[]): Map<string, number> {
const result = new Map<string, number>();
for (const [suite, group] of groupBySuite(runs)) {
// порожня група сюди не потрапить, але поодинокий елемент — так
const total = group.reduce((sum, run) => sum + run.durationMs, 0);
result.set(suite, total / group.length);
}
return result;
}
Крок 4. Перевірка — проганяю вголос. «Порожній вхід → зовнішній reduce не викликається жодного разу, повертається порожня мапа, ділення на нуль не трапляється, ок. Один прогін у сюїті → сума ділиться на один, повертає саму тривалість, ок. Два прогони 100 і 300 → середнє 200, ок».
Що тут читає інтервʼюер:
- Порожня група оброблена конструкцією, а не забута. Всередині циклу
group.lengthзавжди щонайменше один, бо групи створюються тільки для наявних елементів — і це варто сказати вголос, а не сподіватися, що помітять. - Два кроки замість одного «хитрого». Розкладена задача читається й дебажиться простіше; передчасна спроба порахувати все в один прохід — часта причина застрягання.
reduceпояснено. Акумулятор — мапа, кожен елемент кладеться у свій кошик; уміння проговорити це цінується окремо.
Кейс 2. Async на практиці: retry поверх флакі-запиту в Playwright
«Напиши retry для нестабільного запиту» — і одразу продовження: «а тепер прикрути його до реального API-виклику». Ось де теорія стає робочим кодом автоматизатора.
const delay = (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms));
async function retry<T>(
action: () => Promise<T>,
attempts = 3,
delayMs = 500,
): Promise<T> {
let lastError: unknown;
for (let i = 0; i < attempts; i++) {
try {
return await action();
} catch (error) {
lastError = error;
// не спимо після ОСТАННЬОЇ спроби — чекати вже нема сенсу
if (i < attempts - 1) await delay(delayMs * 2 ** i); // експоненційний бекоф
}
}
throw lastError; // прокидаємо останню помилку, а не ковтаємо
}
Тепер той самий хелпер у контексті Playwright API — повторюємо запит, поки ресурс «прогрівається» після :
import { test, expect } from '@playwright/test';
test('статус готовності стає 200 після кількох спроб', async ({ request }) => {
const res = await retry(async () => {
const r = await request.get('https://app.example.com/health');
// кидаємо, щоб retry вважав не-200 невдачею й повторив
if (r.status() !== 200) throw new Error(`health = ${r.status()}`);
return r;
});
expect(res.status()).toBe(200);
});
Що дивитися і чому:
- Умова
i < attempts - 1прибирає зайву паузу. Без неї код чесно спить після фінального провалу перед тим, як кинути помилку, — марна , яку інтервʼюер помічає одразу. throwусерединіaction, а не мовчазнийreturn r.retryреагує лише на викинуту помилку, тож не-200 треба перетворити на виняток; інакше перша ж відповідь503вважатиметься успіхом і повтору не буде.- Retry маскує , а не лікує. У health-чеку прогрівання середовища це доречно, але прикривати retry-єм справжню нестабільність продукту — значить ховати баг; про це варто сказати вголос як про межу застосування.
Кейс 3. Таблиця рішень: який спосіб паралельності обрати
Найчастіша плутанина в async-задачах — коли брати for...of, коли Promise.all, а коли будувати пул із лімітом. Розвʼязує її одне питання: чи залежать виклики один від одного і чи можна перевантажити сервер.
| Ситуація | Правильний інструмент | Чому не інакше |
|---|---|---|
| Кожен наступний виклик залежить від результату попереднього | for...of з await усередині | Promise.all запустив би все відразу й зламав залежність; forEach узагалі не чекає |
| Виклики незалежні, сервер витримає всі одночасно | Promise.all над масивом промісів | Послідовний for...of тут марно повільний — немає причини чекати по черзі |
| Виклики незалежні, але «не більше N водночас» | Пул воркерів над масивом функцій | Promise.all не має ліміту; готові проміси стартують негайно й ліміт їх не притримає |
| Потрібні всі результати, навіть якщо частина впала | Promise.allSettled | Promise.all відхиляється на першій же помилці й губить решту результатів |
Найпідступніший рядок — третій. Ось чому ліміт працює лише над функціями:
async function withLimit<T>(
tasks: (() => Promise<T>)[], // саме функції, не готові проміси!
limit: number,
): Promise<T[]> {
const results: T[] = new Array(tasks.length);
let cursor = 0;
async function worker(): Promise<void> {
while (cursor < tasks.length) {
const index = cursor++;
results[index] = await tasks[index]();
}
}
const pool = Array.from({ length: Math.min(limit, tasks.length) }, worker);
await Promise.all(pool);
return results;
}
Що дивитися і чому:
tasks— масив функцій, а не промісів. Проміс стартує в момент створення, тож якби на вході були вже створені проміси, усі 100 запитів пішли б одразу, іlimitстав би декорацією. Функція відкладає старт до моменту, коли її викличе.- Порядок результатів збережено через
index. Воркери розбирають задачі зі спільного курсора в довільному темпі, але кожен кладе результат за своїм індексом, тож вихід відповідає входу. - Edge cases варто назвати вголос. Порожній список;
limitбільший за кількість задач (тоді воркерів створюється рівно стільки, скільки задач); задача, що падає, — уточни, чи зупиняти весь пул. Навіть недописаний пул із правильно сформульованою ідеєю функцій-задач зараховують.
Що оцінюють на live coding
- Розумію, що дивляться не на сам код, а на мислення: уточнення вимог, бачення , пояснення вголос і самоперевірку.
- Можу проговорити патерн вимоги → edge cases → рішення → перевірка й свідомо застосувати його до будь-якої задачі.
- Тримаю в голові універсальний список меж: порожній вхід, один елемент, дублікати, unicode,
null/undefined, дуже великий вхід.
Рядки
- Знаю різницю між
[...str](за код-поінтами) іstr.split('')(за 16-бітними одиницями) і чому емодзі ламаються саме на другому. - Розумію, що в задачі на паліндром головне питання — не порівняння, а нормалізація: регістр, пробіли, пунктуація.
- Знаю, чому для частотного словника
Mapкращий за обʼєкт: будь-які ключі, немає колізій зі службовими іменами, збережений порядок вставки.
Масиви й Set
- Видаю не думаючи ідіому
[...new Set(items)]для унікальних значень. - Можу знайти саме дублікати через два
Set(бачені та повторені) за один прохідO(n)замість наївногоO(n²). - Роблю перетин масивів ефективним, кладучи один масив у
Setзаради константної перевірки.
Сортування
- Знаю, що
sortбез компаратора сортує як рядки, тож[10, 9, 1].sort()дає[1, 10, 9]. - Памʼятаю, що
sortмутує оригінал, і сортую копію через[...arr].sort(...). - Можу пояснити, що з ES2019
sortгарантовано стабільний, і на це можна спертися при багаторівневому сортуванні.
Тестові дані: фільтрація, групування, агрегація
- Можу згрупувати елементи за ключем через
reduceзMap-акумулятором і пояснити кожен крок. - Знаю про
Object.groupByяк новіший цукор і згадую про його обмежену доступність у середовищах. - Завжди перевіряю порожній масив перед агрегацією, бо середнє без цього дає
NaNвід ділення на нуль.
Async
- Пишу retry, що не спить після останньої спроби, зберігає й прокидає останню помилку і типізований ; памʼятаю, що retry маскує , а не лікує.
- Знаю, чому
awaitусерединіforEachне працює, і роблю послідовність черезfor...of. - Будую ліміт паралельності над масивом функцій, що повертають , а не над готовими промісами.
FizzBuzz і дизайн
- Розумію, що FizzBuzz дають на розширюваність, і виношу правила в дані замість каскаду
if. - Можу пояснити, чому «FizzBuzz» на 15 виходить автоматично зі склеювання слів, а нове правило — це один елемент масиву.
Квіз
Перед стартом
- Питань: 16
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Патерн міркування на live coding для AQA?