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 з'явилися раніше, ніж стандартні поля класів у 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.
Сучасні 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: різні типи гетера й сетера