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

Junior: питання на співбесіді з теми «Функції й класи»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

5 питань

Тип функції складається з типів параметрів і типу результату:

function formatPrice(amount: number, currency: string): string {
  return `${amount.toFixed(2)} ${currency}`;
}

// тип функції окремо - для колбеків і змінних
type Formatter = (amount: number, currency: string) => string;
const uah: Formatter = (amount) => `${amount} грн`;   // параметрів може бути менше

Необов'язковий параметр - знак ?. Усередині функції він має тип T | undefined, тож його треба перевіряти:

function greet(name?: string) {
  return `Привіт, ${name ?? 'гостю'}`;
}

Параметр зі значенням за замовчуванням - тип виводиться зі значення, і всередині функції undefined вже не буде:

function paginate(page = 1, perPage = 20) { /* page: number */ }

Rest-параметр збирає решту аргументів у масив чи кортеж:

function sum(...numbers: number[]): number {
  return numbers.reduce((total, n) => total + n, 0);
}

function log(level: 'info' | 'error', ...messages: unknown[]) {}

Що варто знати:

  • необов'язкові параметри йдуть після обов'язкових;
  • name?: string і name: string | undefined - різне: другий обов'язково передати, хай навіть undefined;
  • тип результату можна не писати - TypeScript виведе його. Для публічних функцій (експорт з модуля, бібліотеки) явний тип корисний: помилка в реалізації не змінить мовчки контракт;
  • функція з колбеком меншої арності сумісна - [1, 2].forEach((n) => ...) не вимагає всіх трьох параметрів колбека.

Об'єкт параметрів замість довгого списку - читабельніше й легше розширюється:

function createUser({ name, role = 'user' }: { name: string; role?: 'user' | 'admin' }) {}

Докладніше в документації: Функції: необов'язкові параметри

Ці три типи описують різні ситуації «функція нічого корисного не повертає».

void - результат функції не використовується:

function logEvent(name: string): void {
  console.log(name);
}

Усередині функції з явно вказаним void повертати значення не можна - return 1 дасть помилку.

Але тип функції з void поводиться інакше: функцію, що щось повертає, можна присвоїти змінній типу () => void:

type Callback = () => void;
const cb: Callback = () => 42;   // дозволено

const items: number[] = [];
[1, 2].forEach((n) => items.push(n));   // push повертає number - і це нормально

Це зроблено навмисно: колбек на кшталт forEach ігнорує результат, і вимагати «нічого не повертати» було б незручно. Наслідок - const result = cb() має тип void, а не number: використовувати його не варто.

undefined - функція повертає значення undefined, і викликач може на це покладатися:

function find(id: number): User | undefined {}

Для «нічого не повертає» в типах функцій зазвичай пишуть void, а undefined - коли значення має сенс (T | undefined).

never - функція ніколи не завершується нормально: кидає виняток або нескінченно виконується.

function fail(message: string): never {
  throw new Error(message);
}

function assertNever(value: never): never {
  throw new Error(`Неочікуване значення: ${value}`);
}

TypeScript використовує never в аналізі потоку: код після виклику fail() вважається недосяжним, а в звуженні типу never означає «варіантів не лишилося» - це основа перевірки вичерпності в switch.

Коротко: void - «результат не цікавить», undefined - «повертає саме undefined», never - «не повертає взагалі».

Докладніше в документації: Функції: тип результату void

TypeScript має модифікатори доступу public, protected, private:

  • public (за замовчуванням) - доступно всім;
  • protected - класу й нащадкам;
  • private - лише самому класу.
class Account {
  private balance = 0;
  protected currency = 'UAH';
  #pin = '1234';   // приватне поле JavaScript
}

Ключова різниця - коли діє обмеження.

private TypeScript - лише перевірка компілятора. Після компіляції це звичайна властивість об'єкта: вона видна в Object.keys, у JSON.stringify, доступна з JavaScript-коду. Навіть у TypeScript є лазівка - доступ через квадратні дужки:

const account = new Account();
account.balance;      // помилка компіляції
account['balance'];   // дозволено - навмисна «аварійна дверцята»

#private - справжня приватність під час виконання. Поле недосяжне ззовні класу в принципі: ні через дужки, ні через Object.keys, ні через рефлексію. Спроба звернутися до #pin поза класом - синтаксична помилка JavaScript.

Коли що обирати:

  • #private - коли потрібна гарантія: бібліотеки, код, який використовують з JavaScript, дані, що не повинні потрапити в серіалізацію;
  • private - якщо достатньо перевірки на етапі компіляції й потрібна гнучкість: тести, що зазирають у стан, сумісність зі старим кодом, бібліотеки, яким потрібен доступ до полів (ORM, серіалізатори).

Нюанси #private:

  • #field in obj - перевірка, що об'єкт створено цим класом;
  • приватні поля не працюють через Proxy (Vue reactive, MobX): метод, викликаний на проксі, падає з TypeError;
  • protected аналога в JavaScript не має - лише перевірка TypeScript.

readonly - окремий модифікатор: поле можна присвоїти лише при оголошенні чи в конструкторі. Теж діє тільки під час компіляції.

Докладніше в документації: Класи: видимість членів

Parameter properties - скорочення TypeScript: модифікатор доступу в параметрі конструктора одночасно оголошує поле класу й присвоює йому значення.

class User {
  constructor(
    public readonly id: number,
    private name: string,
  ) {}
}

// те саме, що
class User {
  public readonly id: number;
  private name: string;

  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }
}

Зручно й коротко - тому цей запис став популярним (Angular, NestJS, багато бекенд-коду на TypeScript).

Проблема: це не «лише типи». Більшість синтаксису TypeScript можна просто стерти - прибрати анотації, і лишиться коректний JavaScript. Parameter properties так не працюють: щоб отримати JavaScript, компілятор має згенерувати код (присвоєння this.id = id).

Те саме з enum (генерує об'єкт), namespace з виконуваним кодом і import x = require().

Чому це стало важливим:

  • Node.js 22.18+/23.6+ виконує .ts файли напряму через «стирання типів» (type stripping) - без компіляції. Синтаксис, що потребує генерації коду, там не підтримується;
  • інструменти на кшталт швидких транспіляторів теж простіше працюють зі «стираним» синтаксисом.

Прапорець erasableSyntaxOnly (TypeScript 5.8+) забороняє такий синтаксис у проєкті - помилка компілятора «This syntax is not allowed when erasableSyntaxOnly is enabled»:

{ "compilerOptions": { "erasableSyntaxOnly": true } }

Чим замінювати:

  • parameter properties - звичайні поля й присвоєння в конструкторі;
  • enum - об'єкт з as const і тип-об'єднання:
const Status = { Draft: 'draft', Published: 'published' } as const;
type Status = (typeof Status)[keyof typeof Status];

Практичний висновок: у новому коді, особливо для Node.js без збирання, варто вмикати erasableSyntaxOnly - тоді TypeScript-код лишається «JavaScript з анотаціями». Parameter properties не помилка, але прив'язують код до компіляції.

Докладніше в документації: Класи: parameter properties

extends - успадкування: клас отримує поля й методи батьківського класу (і їхню реалізацію), може їх перевизначити й доповнити.

class Model {
  save() { /* загальна логіка збереження */ }
}

class Post extends Model {
  title = '';
}

new Post().save();   // метод успадковано

Клас може успадковувати лише один клас.

implements - перевірка відповідності інтерфейсу. Клас обіцяє мати певні члени, але нічого не отримує - реалізацію треба написати самому:

interface Cacheable {
  cacheKey(): string;
}

interface Serializable {
  toJSON(): object;
}

class Product implements Cacheable, Serializable {
  constructor(private id: number) {}

  cacheKey() { return `product:${this.id}`; }
  toJSON() { return { id: this.id }; }
}

Клас може реалізовувати кілька інтерфейсів.

Важливе непорозуміння: implements не змінює тип класу і не впливає на виведення типів його членів. Він лише перевіряє:

interface Checkable {
  check(name: string): boolean;
}

class NameChecker implements Checkable {
  check(s) {   // s - неявно any, а не string з інтерфейсу!
    return s.length > 0;
  }
}

Параметр не отримує тип з інтерфейсу - його треба оголосити явно.

Структурна типізація: клас, який має потрібні методи, і так сумісний з інтерфейсом - навіть без implements. Ключове слово корисне як явний контракт: помилка з'являється в місці оголошення класу, а не там, де його десь передають.

Що обирати:

  • extends - коли є спільна реалізація, а нащадок справді «є різновидом» батька;
  • implements - коли потрібен спільний контракт без спільного коду;
  • для повторного використання поведінки часто краща композиція (передати об'єкт-помічник), ніж глибокі ієрархії успадкування.

Інтерфейс може «розширювати» клас (interface X extends SomeClass) - береться лише його форма, разом із приватними членами, що на практиці трапляється рідко.

Докладніше в документації: Класи: implements