QA Interview Hub · База знань · Тести
TypeScript
Основи: примітиви, any, unknown, never, літерали
Навіщо TypeScript у тестовому фреймворку
TypeScript - це JavaScript плюс статична система типів. Типи існують тільки на етапі компіляції: після транспіляції в JS вони повністю стираються, і в рантаймі нічого не перевіряють. Це ключова думка, яку треба чесно проговорювати на співбесіді: TS ловить помилки в коді, але не гарантує, що відповідь API реально має ту форму, яку ви описали.
Що TS дає автоматизатору на практиці:
- Автодоповнення для Page Object, фікстур, API-клієнтів. Новий інженер бачить, які методи є і які аргументи вони приймають.
- Безпечний рефакторинг. Перейменували поле
accountId у моделі - компілятор покаже всі тести, які зламалися. - Документація в коді. Тип
TransferRequest пояснює контракт краще, ніж коментар. - Ранні помилки: опечатка в назві поля,
undefined там, де очікується рядок, забутий await.
Базові примітиви: string, number, boolean, null, undefined, bigint, symbol. Для масивів - string[] або Array<string>, для кортежів - [string, number].
const baseUrl: string = process.env.BASE_URL ?? 'http://localhost:3000';
const retries: number = 2;
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
const pair: [currency: string, amount: number] = ['UAH', 10050];
Явно анотувати кожну змінну не потрібно: TS виводить тип сам (type inference). Анотації я ставлю на межах: параметри функцій, повернені значення публічних методів, моделі даних.
На співбесіді: TypeScript для мене - це інструмент, який ловить помилки до запуску тестів і робить фреймворк самодокументованим. Але типи стираються в рантаймі, тому дані з API я додатково валідую схемою, а не просто довіряю типу.
any, unknown і never
any вимикає перевірку типів. Зі змінною типу any можна робити що завгодно, і компілятор мовчить. Гірше того, any "заражає" все, до чого торкається: результат anyValue.foo.bar теж any. У тестовому коді це типове джерело помилок, які проявляються тільки при запуску.
unknown - безпечна альтернатива. У змінну unknown можна покласти що завгодно, але використати її без перевірки не можна. Спочатку треба звузити тип: typeof, instanceof, type guard або валідація схемою.
async function readBody(res: Response): Promise<unknown> {
return res.json();
}
const body = await readBody(res);
// body.id; // помилка: 'body' is of type 'unknown'
if (typeof body === 'object' && body !== null && 'id' in body) {
console.log(body.id); // тепер можна
}
never - тип, у якого немає значень. Його повертає функція, яка завжди кидає помилку або ніколи не завершується. Найкорисніше застосування - exhaustive check: якщо в switch по union-типу забули обробити варіант, компілятор покаже помилку.
type Status = 'PENDING' | 'COMPLETED' | 'FAILED';
function label(s: Status): string {
switch (s) {
case 'PENDING': return 'В обробці';
case 'COMPLETED': return 'Успішно';
case 'FAILED': return 'Помилка';
default: {
const unreachable: never = s;
throw new Error(`Unknown status: ${unreachable}`);
}
}
}
Додали статус REVERSED - код перестане компілюватися, поки його не обробите.
На співбесіді: any вимикає перевірки, тому в нашому фреймворку він заборонений правилом ESLint. Для зовнішніх даних я використовую unknown і звужую тип перевіркою, а never - для exhaustive switch, щоб новий статус не пройшов непоміченим.
Літеральні типи, as const і satisfies
Літеральний тип - це тип з одного конкретного значення: 'GET', 200, true. Об'єднання літералів (union) дає дуже точні контракти: type Currency = 'UAH' | 'USD' | 'EUR'. Передати 'usd' маленькими літерами вже не вийде.
Коли ви пишете const method = 'GET', TS виводить літеральний тип 'GET'. Але для let або властивостей об'єкта тип розширюється до string. Щоб зберегти літерали, використовують as const: він робить усі поля readonly і фіксує точні значення.
const ENDPOINTS = {
accounts: '/api/v1/accounts',
transfers: '/api/v1/transfers',
} as const;
type Endpoint = typeof ENDPOINTS[keyof typeof ENDPOINTS];
// '/api/v1/accounts' | '/api/v1/transfers'
const CURRENCIES = ['UAH', 'USD', 'EUR'] as const;
type Currency = typeof CURRENCIES[number]; // 'UAH' | 'USD' | 'EUR'
Другий прийом - масив значень плюс тип з нього. Так у вас одне джерело правди: масив можна використати в параметризованих тестах, а тип - у сигнатурах.
Оператор satisfies (з TS 4.9) перевіряє, що значення відповідає типу, але не "розширює" його до цього типу. Це зручно для конфігів тестових даних:
type UserRole = 'admin' | 'operator' | 'viewer';
const users = {
admin: { login: 'admin@test.io', role: 'admin' },
viewer: { login: 'viewer@test.io', role: 'viewer' },
} satisfies Record<string, { login: string; role: UserRole }>;
users.admin.login; // ок, ключі відомі точно
Замість enum я частіше беру union літералів: він не генерує код у рантаймі і краще поєднується з JSON з API.
На співбесіді: Union літералів я використовую для статусів, валют, HTTP-методів, бо це точний контракт без рантайм-коду. as const фіксує конфіг як незмінний з точними типами, а satisfies перевіряє об'єкт на відповідність типу, не втрачаючи конкретні ключі.