vyvchy
    Теми розділу

    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
    порожнє, межі, unicode, null

    Рішення
    найпростіше, що працює

    Перевірка
    прогнати на межах вголос

    знайшов дірку

    Вимоги
    що на вході, що на виході

    Edge cases
    порожнє, межі, unicode, null

    Рішення
    найпростіше, що працює

    Перевірка
    прогнати на межах вголос

    Проговори це буквально такими словами на співбесіді:

    1. Вимоги. «Уточню: вхід — це завжди рядок? Реверс посимвольний? Регістр важливий?» Дві-три хвилини тут економлять переписування потім.
    2. Edge cases. Назви їх до коду: порожній вхід, один елемент, дублікати, від'ємні числа, дуже великий масив, undefined серед даних. Навіть якщо не оброблятимеш усі — покажи, що бачиш.
    3. Рішення. Спершу найпростіше, що працює й читається. Не оптимізуй передчасно; якщо спитають про складність — тоді й обговориш.
    4. Перевірка. Прогони код подумки на і на кількох краях. Вголос: «порожній рядок → повертає порожній, ок; один символ → сам себе, ок».

    Далі — задачі, кожна крізь цю призму.

    Рядки: реверс, паліндром, частотний словник

    Реверс рядка. Класика, у якій ховається справжнє питання про 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 — MapMap гарантує порядок вставки, приймає ключ будь-якого типу й не має «дефолтних» ключів прототипу.
    • MDN — Nullish coalescing operator (??)?? підставляє значення лише для null і undefined — саме те, що треба для ще не баченого ключа.

    Масиви: дублікати, унікальні, перетин, сортування обʼєктів

    • MDN — SetSet тримає унікальні значення, а 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 — MapMap гарантує порядок вставки, приймає ключ будь-якого типу й не має «дефолтних» ключів прототипу.
    • MDN — Nullish coalescing operator (??)?? підставляє значення лише для null і undefined — саме те, що треба для ще не баченого ключа.

    Async-задачі: retry, послідовність, ліміт паралельності

    • MDN — Using promises (JavaScript Guide) — чотири інструменти композиції запускають проміси одночасно; послідовність доводиться будувати окремо.
    • MDN — PromisePromise.all і сусідні статичні методи: коли виконуються, коли відхиляються, що віддають.
    • MDN — async functionasync завжди повертає проміс, а межа синхронного проходить по першому await.
    • typescript-eslint — no-floating-promises — правило вимагає щось зробити з кожним промісом — включно з масивом промісів, який треба віддати в Promise.all().
    • Google Testing Blog — Flaky Tests at Google and How We Mitigate Them знижує хибні спрацювання, але заохочує ігнорувати флак — тобто маскує його, а не лікує.

    FizzBuzz як задача на дизайн

    • 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 — SetSet тримає унікальні значення, а 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 — SetSet тримає унікальні значення, а has() у середньому швидший за Array.prototype.includes().
    • MDN — Array.prototype.reduce() — дві форми reduce() і TypeError на порожньому масиві без початкового значення.
    • MDN — Using promises (JavaScript Guide) — чотири інструменти композиції запускають проміси одночасно; послідовність доводиться будувати окремо.
    • MDN — Classes (JavaScript reference) — клас як шаблон, що інкапсулює дані разом із кодом, і розкладка його елементів.

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.