# JavaScript live coding: типові задачі з повним розбором

> П’ять задач, які реально дають на live coding: debounce, groupBy, flatten, EventEmitter і chunk. Для кожної – уточнювальні питання, рішення на TypeScript, edge cases і як це тестувати.

- Автор: Юра Скиба (https://cookiesoftware.io)
- Опубліковано: 2026-09-24
- Категорія: JavaScript і TypeScript
- Canonical: https://cookiesoftware.io/blog/javascript-live-coding-zadachi

---

**Коротка відповідь.** На live coding оцінюють не «знав/не знав задачу», а процес: як ти уточнюєш вимоги, чи пишеш спершу просту робочу версію, чи бачиш крайні випадки, чи можеш пояснити складність. Нижче п'ять задач, що покривають більшість реальних співбесід рівня Junior–Middle. Для кожної – однакова структура: умова → що уточнити → рішення → edge cases → як тестувати. Проходь їх саме в цьому форматі, а не «глянув рішення – зрозумів».

## Загальний алгоритм на будь-якій задачі

1. Переформулюй умову своїми словами і постав 1–2 уточнювальні питання.
2. Назви сигнатуру функції та типи до того, як писати тіло.
3. Напиши найпростішу робочу версію, скажи про це вголос.
4. Пройдись по edge cases і полагодь, що треба.
5. Заверши фразою «тестував би так: …» – навіть якщо тестів не просили.

## Задача 1. debounce

**Умова:** реалізуй `debounce(fn, delay)` – функція викликається лише після паузи `delay` мс від останнього виклику.

**Що уточнити:** чи потрібен виклик із першого разу (leading), чи зберігати this і аргументи, чи потрібен метод скасування.

```ts
function debounce<Args extends unknown[]>(
  fn: (...args: Args) => void,
  delay: number
): ((...args: Args) => void) & { cancel: () => void } {
  let timer: ReturnType<typeof setTimeout> | undefined;

  const debounced = (...args: Args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
  debounced.cancel = () => clearTimeout(timer);

  return debounced;
}
```

**Edge cases:** повторний виклик до спливання таймера (попередній має скасуватись – саме для цього `clearTimeout`); виклик `cancel` до першого спрацювання; `delay = 0` (усе одно асинхронно – див. [event loop](/blog/event-loop-javascript)).

**Як тестувати:** фейкові таймери (`vi.useFakeTimers()` у vitest): викликати тричі поспіль, промотати час, перевірити один виклик з останніми аргументами.

**Типова помилка:** забути `clearTimeout` – тоді це не debounce, а «відкладений виклик кожного разу».

## Задача 2. groupBy

**Умова:** `groupBy(items, keyFn)` – розбий масив на групи за ключем.

**Що уточнити:** що повертати – об'єкт чи Map; чи може ключ бути не рядком.

```ts
function groupBy<T, K extends string | number>(
  items: T[],
  keyFn: (item: T) => K
): Record<K, T[]> {
  const result = {} as Record<K, T[]>;
  for (const item of items) {
    const key = keyFn(item);
    (result[key] ??= []).push(item);
  }
  return result;
}

// groupBy(['one', 'two', 'six'], (w) => w.length)
// => { 3: ['one', 'two', 'six'] }
```

**Edge cases:** порожній масив (порожній об'єкт, не помилка); ключі-числа стають рядковими ключами об'єкта; якщо ключем може бути об'єкт – потрібен Map, і це гарний привід сказати про різницю (детальніше – у статті про [структури даних](/blog/javascript-struktury-danykh)).

**Як тестувати:** групування за довжиною слова, за булевим предикатом, порожній вхід, стабільність порядку всередині групи.

**Типова помилка:** `result[key].push(...)` без перевірки існування групи – TypeError на першому елементі групи.

## Задача 3. flatten

**Умова:** розплющ вкладений масив довільної глибини: `flatten([1, [2, [3, [4]]]]) → [1, 2, 3, 4]`.

**Що уточнити:** глибина обмежена чи довільна; чи можна використовувати вбудований `Array.prototype.flat`.

```ts
type Nested<T> = (T | Nested<T>)[];

function flatten<T>(input: Nested<T>): T[] {
  const result: T[] = [];
  for (const item of input) {
    if (Array.isArray(item)) {
      result.push(...flatten(item));
    } else {
      result.push(item);
    }
  }
  return result;
}
```

Сильний хід: спершу сказати «у продакшні – `arr.flat(Infinity)`», а потім написати ручну версію, бо питають саме її. Якщо інтерв'юер попросить без рекурсії – ітеративна версія зі стеком: знімай елемент, масиви розгортай назад у стек, інакше додавай у результат.

**Edge cases:** порожні масиви на будь-якому рівні; дуже глибока вкладеність (рекурсія може впертись у stack overflow – тому й існує ітеративний варіант); значення `undefined`/`null` як легальні елементи.

**Як тестувати:** глибина 0/1/довільна, порожні підмасиви, великий вхід на переповнення стека для рекурсивної версії.

## Задача 4. EventEmitter

**Умова:** клас з `on`, `off`, `emit`.

**Що уточнити:** чи може бути кілька слухачів однієї події; що робить `off` без конкретного слухача; чи потрібен `once`.

```ts
type Listener = (...args: unknown[]) => void;

class EventEmitter {
  private listeners = new Map<string, Set<Listener>>();

  on(event: string, listener: Listener): () => void {
    if (!this.listeners.has(event)) this.listeners.set(event, new Set());
    this.listeners.get(event)!.add(listener);
    return () => this.off(event, listener);
  }

  off(event: string, listener: Listener): void {
    this.listeners.get(event)?.delete(listener);
  }

  emit(event: string, ...args: unknown[]): void {
    for (const listener of [...(this.listeners.get(event) ?? [])]) {
      listener(...args);
    }
  }
}
```

Дві деталі, які відрізняють Middle-відповідь: `on` повертає функцію відписки (патерн, знайомий кожному, хто писав useEffect-cleanup у React – див. [React live coding](/blog/react-live-coding-typescript)); в `emit` ітеруємо **копію** набору, щоб слухач, який відписується під час події, не ламав ітерацію.

**Edge cases:** emit події без слухачів (тиша, не помилка); подвійна підписка того самого слухача (Set гарантує один запис); відписка всередині emit.

**Як тестувати:** підписка → emit → перевірка викликів і аргументів; відписка через повернуту функцію; два слухачі на одну подію.

## Задача 5. chunk

**Умова:** `chunk([1,2,3,4,5], 2) → [[1,2],[3,4],[5]]`.

**Що уточнити:** що робити з size ≤ 0 – кинути помилку чи повернути порожній масив (домовся і зафіксуй словами).

```ts
function chunk<T>(items: T[], size: number): T[][] {
  if (!Number.isInteger(size) || size <= 0) {
    throw new RangeError(`size має бути додатним цілим, отримано ${size}`);
  }
  const result: T[][] = [];
  for (let start = 0; start < items.length; start += size) {
    result.push(items.slice(start, start + size));
  }
  return result;
}
```

**Edge cases:** порожній масив → `[]`; size більший за довжину → один шматок; неціле/від'ємне size → явна помилка з повідомленням (валідація вхідних даних – дешевий спосіб показати продакшн-звичку).

**Як тестувати:** рівний і нерівний поділ, size = 1, size > length, невалідний size (перевірка на кинуту помилку).

## Бонус. retry для нестабільного запиту

Частий фінал співбесіди «на подумати»: повторюй асинхронну операцію до `attempts` разів із паузою між спробами.

**Що уточнити:** пауза фіксована чи зростає; які помилки ретраїти (усі чи лише мережеві).

```ts
async function retry<T>(
  operation: () => Promise<T>,
  attempts: number,
  delayMs: number
): Promise<T> {
  let lastError: unknown;
  for (let attempt = 1; attempt <= attempts; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error;
      if (attempt < attempts) {
        await new Promise((resolve) => setTimeout(resolve, delayMs * attempt));
      }
    }
  }
  throw lastError;
}
```

**Edge cases:** `attempts = 1` (жодного повтору – просто виклик); операція, що падає завжди (має вилетіти **остання** помилка, не перша); пауза після останньої невдачі не потрібна – зверни увагу на умову `attempt < attempts`.

**Як тестувати:** мок-операція, що падає N разів і потім успішна; перевірка кількості викликів; фейкові таймери для пауз. Механіка «чому await із setTimeout працює саме так» – у [розборі event loop](/blog/event-loop-javascript), а патерни навколо промісів – у статті про [async/await і Promise](/blog/async-await-promise).

## Як працювати з цією добіркою

Один день – одна задача, під таймер 25–30 хвилин, із проговорюванням уголос усіх п'яти кроків алгоритму. Наступного дня – спершу відтворити вчорашню з нуля, потім нова. Написання тестів до власних рішень – окрема навичка, яку теж перевіряють: як саме їх будувати – у статті [тести для live-coding задач](/blog/testy-dlia-live-coding). Типи в сигнатурах вище – базовий TypeScript; якщо генерики поки лякають, спершу пройди [розбір TypeScript generics](/blog/typescript-generics). Довідник по всіх вбудованих методах, які тут використані, – [MDN JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript).
