Питання на співбесіді: Функції й класи
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
14 питань
Тип функції складається з типів параметрів і типу результату:
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 - «не повертає взагалі».
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(Vuereactive, 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 не помилка, але прив'язують код до компіляції.
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) - береться лише його форма, разом із приватними членами, що на практиці трапляється рідко.
Перевантаження описують кілька способів викликати функцію з різними типами результату залежно від аргументів. Складаються з кількох сигнатур і однієї реалізації:
function parse(value: string): number;
function parse(value: number): string;
function parse(value: string | number): string | number {
return typeof value === 'string' ? Number(value) : String(value);
}
parse('42'); // number
parse(42); // string
Правила:
- реалізація ззовні не видна - викликати функцію можна лише за сигнатурами перевантажень. Сигнатура реалізації має бути сумісною з усіма ними;
- TypeScript перебирає перевантаження по черзі й бере перше відповідне - тому конкретніші ставлять вище за загальніші;
- реалізація сама перевіряє, з яким варіантом її викликали (
typeof,in), - компілятор не перевіряє, що кожна гілка повертає «правильний» тип для своєї сигнатури.
Головна пастка - аргумент-об'єднання:
declare const input: string | number;
parse(input); // помилка: жодне перевантаження не приймає string | number
Кожне перевантаження приймає лише свій тип, а об'єднання не підходить жодному. Доводиться додавати ще одну сигнатуру (value: string | number): string | number.
Коли краще без перевантажень:
- результат не залежить від типу аргументу - звичайний тип-об'єднання;
- результат залежить від аргументу передбачувано - generic чи умовний тип:
function first<T>(items: T[]): T | undefined {
return items[0];
}
- різні способи виклику з різною кількістю параметрів - часто краще об'єкт параметрів чи кілька окремих функцій з виразними назвами (
parseNumber,formatNumber).
Коли перевантаження доречні:
- типи для API, яке вже так влаштоване - бібліотеки,
.d.tsдля JavaScript-коду (document.createElement('canvas')повертаєHTMLCanvasElement- перевантаження в типах DOM); - залежність результату від літерального значення аргументу, яку незручно виразити умовним типом.
Перевантаження методів класу пишуться так само, а в типах об'єктів - кількома сигнатурами виклику.
У JavaScript значення this залежить від способу виклику, а не від місця оголошення. TypeScript дає змогу описати очікуваний this - і перевірити, що функцію викликають правильно.
Параметр this - перший «фіктивний» параметр, який не існує під час виконання:
function disable(this: HTMLButtonElement, event: MouseEvent) {
this.disabled = true;
}
button.addEventListener('click', disable); // ок: this - кнопка
const handler = disable;
handler(new MouseEvent('click')); // помилка: this має тип void, а не HTMLButtonElement
У скомпільованому JavaScript параметр this зникає.
Методи класів - TypeScript знає, що this - екземпляр класу. Але при «відриві» методу від об'єкта this губиться під час виконання:
class Counter {
count = 0;
increment() { this.count++; }
}
const c = new Counter();
const inc = c.increment;
inc(); // TypeError під час виконання: this - undefined
Рішення:
- стрілкова функція як поле -
thisзахоплюється при створенні:increment = () => { this.count++; };(ціна - окрема функція на кожен екземпляр, і її немає в прототипі); bindу конструкторі;- параметр
this: Counterу методі - тоді TypeScript сам покаже помилку при виклику без об'єкта.
this як тип результату - для ланцюжків викликів, що коректно працюють у нащадках:
class QueryBuilder {
where(column: string, value: unknown): this {
// ...
return this;
}
}
class PostQuery extends QueryBuilder {
published(): this { return this.where('status', 'published'); }
}
new PostQuery().where('id', 1).published(); // where повернув PostQuery, а не QueryBuilder
Тип-перевірка this is T - type guard для методів:
class Shape {
isCircle(): this is Circle { return this instanceof Circle; }
}
ThisParameterType<F> і OmitThisParameter<F> - утиліти, щоб отримати або прибрати тип this з типу функції (корисно для обгорток над методами).
Абстрактний клас не можна створити напряму (new), лише успадкувати. Він може містити і реалізацію, і абстрактні члени, які зобов'язаний реалізувати нащадок.
abstract class Notifier {
abstract send(to: string, message: string): Promise<void>;
async notifyAll(recipients: string[], message: string) {
for (const to of recipients) {
await this.send(to, message);
}
}
}
class EmailNotifier extends Notifier {
async send(to: string, message: string) {
// реалізація відправки
}
}
new Notifier(); // помилка: абстрактний клас
new EmailNotifier(); // ок
Нащадок, що не реалізував усі абстрактні члени, теж мусить бути abstract.
Чим відрізняється від інтерфейсу:
| Абстрактний клас | Інтерфейс | |
|---|---|---|
| реалізація методів | може мати | ні |
| поля зі значеннями, конструктор | так | ні |
модифікатори protected, private |
так | ні |
| існує під час виконання | так (це клас JavaScript) | ні (зникає при компіляції) |
| кількість на клас | лише один (extends) |
скільки завгодно (implements) |
instanceof |
працює | неможливо |
Коли абстрактний клас:
- є спільна реалізація, а нащадки відрізняються окремими кроками - шаблонний метод: загальний алгоритм у базовому класі, змінні кроки - абстрактні;
- потрібен спільний стан чи конструктор;
- потрібна перевірка
instanceof.
Коли інтерфейс:
- потрібен лише контракт - форма об'єкта чи класу;
- клас має відповідати кільком контрактам;
- об'єкти можуть бути не екземплярами класів (звичайні об'єкти, моки в тестах).
Абстрактні конструктори в типах - щоб прийняти «будь-який підклас», а не сам абстрактний клас:
function register(Ctor: abstract new () => Notifier) {}
Обережно з глибокими ієрархіями: абстрактний клас, від якого успадковують усе, з часом обростає методами «для деяких нащадків». Для повторного використання поведінки часто краща композиція - передати залежність через конструктор.
Коли клас-нащадок перевизначає метод батька, ззовні не видно, чи це свідоме перевизначення, чи збіг назв. А при рефакторингу батьківського класу перевизначення може тихо зламатися.
Модифікатор override робить намір явним:
class Model {
toArray(): Record<string, unknown> { return {}; }
}
class User extends Model {
override toArray() {
return { ...super.toArray(), name: this.name };
}
}
Що перевіряє компілятор:
- метод з
overrideмусить існувати в батьківському класі. Якщо батьківськийtoArrayперейменували наtoObject, у нащадку з'являється помилка - замість тихо «осиротілого» методу, який більше ніхто не викликає; - з прапорцем
noImplicitOverride- навпаки: перевизначення безoverrideтеж стає помилкою («This member must have an 'override' modifier because it overrides a member in the base class»).
{ "compilerOptions": { "noImplicitOverride": true } }
Які помилки це ловить:
- перейменування в батьківському класі - нащадок продовжує «перевизначати» метод, якого вже немає;
- випадкове перевизначення - у нащадку додали метод
save(), не помітивши, що такий уже є в базовому класі, і зламали його логіку; - друкарські помилки -
toArary()зoverrideодразу дасть помилку.
Аналогія з PHP: атрибут #[\Override] з PHP 8.3 робить те саме - перевіряє, що метод справді перевизначає батьківський.
Що варто знати:
overrideпрацює для методів, властивостей і аксесорів;- для реалізації абстрактних членів
overrideдозволений, але навіть зnoImplicitOverrideне обов'язковий - багато команд усе одно пишуть його для однаковості; - модифікатор зникає при компіляції - на виконання не впливає.
Рекомендація: вмикати noImplicitOverride у проєктах, де є успадкування класів. Це ще один прапорець, який дешево ловить реальні помилки і не входить у strict.
Клас, як і функція, може мати параметри типу - тоді один клас працює з різними типами даних зі збереженням перевірок.
class Repository<T extends { id: number }> {
#items = new Map<number, T>();
save(item: T): void {
this.#items.set(item.id, item);
}
find(id: number): T | undefined {
return this.#items.get(id);
}
all(): T[] {
return [...this.#items.values()];
}
}
const users = new Repository<User>();
users.save({ id: 1, name: 'Оля' });
users.find(1)?.name; // тип User
Обмеження extends гарантує, що в T є id - інакше item.id всередині класу був би помилкою.
Виведення параметра типу з конструктора:
class Box<T> {
constructor(public value: T) {}
}
const box = new Box(42); // Box<number> - тип виведено
Чому статичні члени не можуть використовувати T:
class Repository<T> {
static defaultItem: T; // помилка: static members cannot reference class type parameters
}
Параметр типу належить екземпляру: new Repository<User>() і new Repository<Post>() - різні «версії» з різними T. А статичний член один на весь клас - для всіх екземплярів одразу. Яким має бути T у Repository.defaultItem? Відповіді немає.
Якщо статичному методу потрібен generic, він оголошує власний параметр типу:
class Repository<T> {
static of<U extends { id: number }>(items: U[]): Repository<U> {
const repo = new Repository<U>();
items.forEach((item) => repo.save(item));
return repo;
}
}
Корисні прийоми:
- значення за замовчуванням для параметра:
class Cache<V = string>; - кілька параметрів:
class Store<State, Action extends { type: string }>; thisяк тип у методах - для ланцюжків, що зберігають тип нащадка.
Обмеження: параметри типу зникають під час виконання. Усередині класу не можна написати new T() чи x instanceof T - якщо потрібен конструктор, його передають явно: constructor(private make: new () => T).
У 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: різні типи гетера й сетера