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 перевіряє об'єкт на відповідність типу, не втрачаючи конкретні ключі.