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

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

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

4 питання

У TypeScript дві несумісні реалізації декораторів.

Стандартні декоратори (TypeScript 5.0+, відповідають пропозиції TC39) - працюють без прапорців:

function logged<This, Args extends unknown[], R>(
  target: (this: This, ...args: Args) => R,
  context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => R>,
) {
  return function (this: This, ...args: Args): R {
    console.log(`виклик ${String(context.name)}`);
    return target.call(this, ...args);
  };
}

class OrderService {
  @logged
  place(id: number) { /* ... */ }
}

Декоратор отримує значення (метод, поле, клас) і об'єкт context: kind, name, static, private, addInitializer(), access, metadata. Повертає заміну чи нічого.

Ключове слово accessor - поле з автоматичними гетером і сетером, які декоратор може перехопити (основа реактивних полів у Lit, MobX):

class Counter {
  @observable accessor count = 0;
}

Експериментальні декоратори (experimentalDecorators: true) - стара реалізація ранньої пропозиції. Сигнатура інша: (target, propertyKey, descriptor). На них побудовані Angular, NestJS, TypeORM, class-validator, Inversify.

function old(target: any, key: string, descriptor: PropertyDescriptor) {}

Без прапорця такий декоратор не скомпілюється: TypeScript перевіряє його як стандартний і повідомляє, що під час виконання він отримає 2 аргументи, а очікує 3.

Відмінності, що мають значення:

  • декоратори параметрів (constructor(@Inject() service: X)) є лише в експериментальних - стандарт їх не має. Саме на них тримається впровадження залежностей у NestJS і Angular;
  • emitDecoratorMetadata (метадані типів для DI, reflect-metadata) - лише з експериментальними;
  • метадані в стандарті - context.metadata і Symbol.metadata, без автоматичних типів параметрів;
  • порядок виконання й семантика ініціалізації полів відрізняються.

Що обирати:

  • фреймворк вимагає експериментальних (Angular, NestJS) - лишатися на них, доки фреймворк не перейде;
  • новий код без таких фреймворків - стандартні декоратори: вони стануть частиною JavaScript і не залежать від прапорця компілятора.

Стан стандарту: пропозиція на стадії 3; рушії браузерів і Node.js нативно декоратори ще не виконують - TypeScript компілює їх у допоміжний код (__esDecorate). Тому декоратори несумісні з erasableSyntaxOnly-підходом «просто стерти типи».

Докладніше в документації: TypeScript 5.0: декоратори

Поля класу в TypeScript з'явилися раніше, ніж стандартні поля класів у JavaScript, і TypeScript довго компілював їх як присвоєння в конструкторі. Стандарт же визначає поля через Object.defineProperty - з іншою семантикою.

useDefineForClassFields перемикає на стандартну семантику. Він увімкнений за замовчуванням, коли target - ES2022 чи новіший (а з TypeScript 6 типова ціль - поточна версія ECMAScript, тож для нових проєктів це майже завжди так).

Де різниця стає помітною:

1. Поле без ініціалізатора в нащадку затирає значення з батька:

class Base {
  name: string;
  constructor() {
    this.name = 'base';
    this.init();
  }
  init() {}
}

class Child extends Base {
  name!: string;   // лише уточнення типу
}

new Child().name;   // зі стандартною семантикою - undefined!

Оголошене в нащадку поле визначається заново (зі значенням undefined) після батьківського конструктора. Рішення - declare name: string;: це оголошення лише для типів, без поля під час виконання.

Компілятор попереджає про обидва випадки нижче: «Property 'name' will overwrite the base property... add a 'declare' modifier» (TS2612) і «'value' is defined as an accessor in class..., but is overridden here as an instance property» (TS2610). Ці помилки не варто глушити - вони описують реальну зміну поведінки.

2. Поля перекривають сетери батька:

class Base {
  set value(v: number) { console.log('setter', v); }
}
class Child extends Base {
  value = 1;   // стандарт: власна властивість, сетер не викликається
}

3. Порядок ініціалізації з parameter properties: поля ініціалізуються до тіла конструктора, і поле, що використовує parameter property, може побачити undefined.

4. Бібліотеки з декораторами чи реактивністю (старі MobX, деякі ORM), що покладалися на присвоєння й сетери прототипу, ламаються зі стандартною семантикою.

Що робити:

  • у новому коді - лишати стандартну семантику (так поводиться і браузер, і Node.js, і будь-який інший транспілятор);
  • для перевизначення типу поля в нащадку - declare, а не повторне оголошення;
  • при міграції старого коду - перевірити класи з успадкуванням полів і сетерів; тимчасово можна вимкнути прапорець, але це розходження з реальним JavaScript.

Чому це не «дрібниця налаштувань»: код, що працює однаково в тестах (одна ціль компіляції) і по-різному в продакшені (інша), - одна з найнеочевидніших помилок при зміні target.

Докладніше в документації: useDefineForClassFields

Сучасні Node.js (з 22.18 і 23.6) запускають .ts файли напряму: перед виконанням вони просто стирають анотації типів, не перевіряючи їх і не компілюючи. Так само працюють швидкі інструменти на кшталт Bun, Deno та трансформацій у збирачах.

Що можна стерти - більша частина TypeScript: анотації, інтерфейси, type, generics, as, satisfies, import type. Після видалення лишається коректний JavaScript.

Що стерти неможливо - синтаксис, якому потрібна генерація коду:

  • enum - створює об'єкт під час виконання;
  • namespace з кодом усередині;
  • parameter properties (constructor(private name: string)) - генерують присвоєння;
  • import x = require('...'), export =;
  • старі декоратори з emitDecoratorMetadata.

Прапорець erasableSyntaxOnly (TypeScript 5.8+) робить такий синтаксис помилкою компіляції - код гарантовано запуститься через стирання типів:

{
  "compilerOptions": {
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "noEmit": true
  }
}

verbatimModuleSyntax доповнює його: імпорти лише типів мають бути явно позначені import type, щоб стирання не лишило імпорт неіснуючого значення.

Замінники:

// замість enum
const Role = { Admin: 'admin', Editor: 'editor' } as const;
type Role = (typeof Role)[keyof typeof Role];

// замість parameter properties
class User {
  readonly id: number;
  constructor(id: number) { this.id = id; }
}

Як це змінює роль tsc: компілятор дедалі частіше виконує лише перевірку типів (noEmit), а виконання й збирання роблять інші інструменти. TypeScript 7 (нативний компілятор на Go) робить таку перевірку в рази швидшою - і поділ «типи перевіряє tsc, код запускає рушій» стає типовою схемою.

Обмеження стирання типів у Node.js:

  • типи не перевіряються під час запуску - помилки типів знайде лише tsc;
  • tsconfig ігнорується - paths, baseUrl (у TypeScript 7 його вже немає) не працюють;
  • імпорти з розширенням .ts - потрібно allowImportingTsExtensions у tsconfig або перезапис розширень (rewriteRelativeImportExtensions) при компіляції бібліотеки.

Висновок: для коду, який може запускатися без збирання (скрипти, сервіси на Node.js), «стираний» TypeScript - розумний стандарт. Для фронтенду через збирач обмеження м'якші, але й там enum і parameter properties дедалі частіше замінюють.

Докладніше в документації: TypeScript 5.8: erasableSyntaxOnly

Аксесори (гетер і сетер) у класах і об'єктах виглядають ззовні як звичайна властивість, але виконують код при читанні й записі.

class Temperature {
  #celsius = 0;

  get fahrenheit(): number {
    return this.#celsius * 1.8 + 32;
  }

  set fahrenheit(value: number) {
    this.#celsius = (value - 32) / 1.8;
  }
}

Правила TypeScript:

  • властивість лише з гетером автоматично readonly - запис дає помилку;
  • з TypeScript 5.1 гетер і сетер можуть мати непов'язані типи (раніше тип гетера мусив бути сумісним з типом сетера).

Навіщо різні типи - сетер приймає ширше, гетер повертає нормалізоване:

class Field {
  #value = '';

  get value(): string {
    return this.#value;
  }

  set value(input: string | number | null) {
    this.#value = input === null ? '' : String(input);
  }
}

const field = new Field();
field.value = 42;          // дозволено
field.value.toUpperCase(); // завжди string

Так влаштовано чимало API браузера: властивість приймає ширший набір значень (рядок, null), а повертає нормалізований рядок. Опис таких API в .d.ts став точнішим саме завдяки цій можливості.

Те саме в інтерфейсах і типах об'єктів:

interface Thing {
  get size(): number;
  set size(value: number | string | boolean);
}

Ключове слово accessor (стандартні декоратори) - коротший запис поля з автоматичними гетером і сетером над прихованим сховищем: accessor count = 0. Корисне разом із декораторами, що перехоплюють доступ.

Що варто пам'ятати:

  • дорогі гетери виглядають як дешеві поля - виклик у циклі може приховано коштувати багато;
  • побічні ефекти в гетері - погана практика: читання не повинно змінювати стан;
  • серіалізація: JSON.stringify не бачить гетерів класу (вони в прототипі) - потрібен toJSON();
  • поля проти аксесорів у нащадках: з useDefineForClassFields поле в нащадку перекриває сетер батька - сетер не викликається.

Докладніше в документації: TypeScript 5.1: різні типи гетера й сетера