QA Interview Hub · База знань · Тести
TypeScript
Обʼєктні типи: interface і type, optional, readonly
interface чи type
Обидва способи описують форму об'єкта, і для моделей API різниця часто непомітна.
interface Account {
id: string;
iban: string;
balance: number; // у копійках
}
type Transfer = {
from: string;
to: string;
amount: number;
};
Ключові відмінності:
type може описати будь-що: union, кортеж, примітив, функцію, mapped і conditional типи. interface описує тільки форму об'єкта або класу.interface підтримує declaration merging: два оголошення з однаковим ім'ям зливаються. Саме так розширюють типи бібліотек, наприклад додають поле в глобальний інтерфейс.- Наслідування:
interface B extends A, у типів - перетин type B = A & { ... }. При конфлікті полів extends дає зрозумілу помилку одразу, а перетин може мовчки дати never для поля. - Класи можуть
implements і інтерфейс, і об'єктний тип.
interface ApiError { code: string; message: string }
interface ValidationError extends ApiError {
fields: string[];
}
type Result = { ok: true; data: Account } | { ok: false; error: ApiError };
Практичне правило, якого я дотримуюсь: interface для моделей даних і контрактів класів (Page Object, клієнти), type для union, утиліт і всього складнішого. Головне - консистентність у проєкті, а не "правильний" вибір.
На співбесіді: Для об'єктних моделей різниці майже немає. type потрібен для union, кортежів і просунутих типів, а interface вміє declaration merging і дає зрозуміліші помилки при наслідуванні. У проєкті я тримаюся однієї конвенції.
Optional, undefined і null
Знак ? робить поле необов'язковим: middleName?: string означає, що поля може не бути взагалі, або воно undefined. Це не те саме, що middleName: string | undefined: у другому випадку поле обов'язкове, але може мати значення undefined.
interface Customer {
id: string;
email: string;
phone?: string; // може бути відсутнім
closedAt: string | null; // поле є завжди, але може бути null
}
Для тестування API ця різниця принципова. Якщо контракт каже, що closedAt завжди присутнє і може бути null, а бекенд почав пропускати поле, - це регресія контракту. Тип має відображати реальний контракт, а не "що зручніше".
Опції компілятора, які посилюють перевірки:
strictNullChecks (входить у strict) - без нього null і undefined можна присвоїти будь-якому типу, і половина користі TS зникає.exactOptionalPropertyTypes - забороняє явно присвоїти undefined полю з ?, якщо undefined не вказаний у типі.noUncheckedIndexedAccess - доступ за індексом arr[0] або record[key] дає T | undefined. Дуже корисно, бо rows[0].amount на порожньому масиві - класичне падіння тесту з незрозумілою помилкою.
Працюючи з optional, я використовую optional chaining і nullish coalescing:
const phone = customer.phone ?? 'не вказано';
const city = customer.address?.city;
Небезпечний прийом - non-null assertion customer.phone!. Він каже компілятору "повір мені" і нічого не перевіряє в рантаймі. У тестах краще явна перевірка через expect(...).toBeDefined() або assertion-функцію.
На співбесіді: ? означає, що поля може не бути, а | null - що поле є, але порожнє. Для API-контрактів це різні речі, і я відображаю їх точно. Я вмикаю strict і noUncheckedIndexedAccess і уникаю оператора !.
readonly і незмінність
readonly забороняє перезапис поля після створення об'єкта. Це перевірка компілятора, у рантаймі об'єкт залишається звичайним і змінюваним.
interface TestUser {
readonly id: string;
readonly email: string;
balance: number;
}
const user: TestUser = { id: 'u1', email: 'a@test.io', balance: 0 };
// user.id = 'u2'; // помилка компіляції
Для масивів є readonly string[] або ReadonlyArray<string>: недоступні push, splice, sort та інші методи, що мутують масив. Утиліта Readonly<T> робить readonly всі поля верхнього рівня.
Важливий нюанс - readonly поверхневий. Вкладені об'єкти залишаються змінюваними:
const config: Readonly<{ api: { timeout: number } }> = { api: { timeout: 5000 } };
// config.api = {...}; // помилка
config.api.timeout = 1; // компілюється!
Для глибокої незмінності використовують as const на літералах (він робить readonly на всіх рівнях) або пишуть власний тип DeepReadonly. Справжню рантайм-заморозку дає Object.freeze, але він теж поверхневий.
Де це корисно в автоматизації:
- Спільні тестові дані і конфіги. Якщо один тест мутує спільний об'єкт, інший тест падає залежно від порядку запуску - класичний flaky.
readonly ловить таку мутацію на етапі компіляції. - Константи ендпоінтів, ролей, лімітів.
- Параметри функцій-хелперів:
function sum(amounts: readonly number[]) гарантує, що хелпер не відсортує масив викликача на місці.
На співбесіді: readonly - це захист на рівні компілятора, він поверхневий і в рантаймі нічого не блокує. Я використовую його для спільних тестових даних і конфігів, щоб тест випадково не змінив об'єкт, від якого залежать інші тести.