QA Interview Hub · База знань · Тести

TypeScript

Модулі й tsconfig

ES Modules і CommonJS

У Node співіснують дві системи модулів:

  • CommonJS: require() і module.exports. Історично стандарт Node.
  • ES Modules: import і export. Стандарт мови.

Як Node вирішує, яка система: файли .mjs - завжди ESM, .cjs - завжди CommonJS, а .js - залежно від поля "type" у найближчому package.json ("module" або "commonjs", за замовчуванням CommonJS).

У TS-коді ми майже завжди пишемо синтаксис import/export, а компілятор або раннер перетворює його в потрібний формат. Тому плутанина виникає не в коді тестів, а в конфігурації: помилки на кшталт "require is not defined in ES module scope" або "Cannot use import statement outside a module".

Корисні речі:

import type { Page, Locator } from '@playwright/test'; // тільки тип, стирається
import { test, expect } from '@playwright/test';

export { LoginPage } from './login-page';      // re-export, barrel-файл
export type { Account } from './models';

import type гарантує, що імпорт буде повністю видалений при компіляції. Це важливо при опції verbatimModuleSyntax, яка вимагає явно позначати типові імпорти, і допомагає уникати циклічних залежностей у рантаймі.

Named export я віддаю перевагу перед default: назва фіксована, автоімпорт і рефакторинг працюють краще.

Playwright сам транспілює TS-файли тестів, тому окремий крок збірки не потрібен. Але він не перевіряє типи: тест з помилкою типів запуститься. Тому в CI я додаю окремий крок npx tsc --noEmit.

На співбесіді: У Node є CommonJS з require і ES Modules з import, і вибір для .js визначає поле type у package.json. Playwright транспілює TS сам, але типи не перевіряє, тому в CI потрібен окремий крок tsc --noEmit.

tsconfig для тестового проєкту

tsconfig.json задає, як компілятор перевіряє і збирає код. Для тестового проєкту на Playwright мінімальний розумний конфіг:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noEmit": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "baseUrl": ".",
    "paths": { "@pages/*": ["src/pages/*"], "@api/*": ["src/api/*"] }
  },
  "include": ["tests", "src", "playwright.config.ts"]
}

Що означають ключові опції:

  • strict - вмикає групу перевірок: strictNullChecks, noImplicitAny, strictFunctionTypes, useUnknownInCatchVariables та інші. Без нього TS дає мало користі.
  • target - до якої версії JS компілювати синтаксис. Для Node 20+ підходить ES2022.
  • module і moduleResolution - формат модулів і правила пошуку імпортів. NodeNext відповідає поведінці Node, Bundler - для коду, що проходить через бандлер.
  • noEmit - тільки перевірка типів, без генерації JS. Запуск виконує Playwright.
  • skipLibCheck - не перевіряти .d.ts файли залежностей, пришвидшує компіляцію.
  • paths - аліаси імпортів замість ../../../pages. Playwright підтримує paths з tsconfig.
  • resolveJsonModule - дозволяє імпортувати JSON з тестовими даними з типом.

Важливо: при module: NodeNext у ESM-проєкті відносні імпорти вимагають розширення .js у шляху, навіть якщо файл .ts. Це часто дивує новачків.

На співбесіді: У тестовому проєкті я обов'язково вмикаю strict і noUncheckedIndexedAccess, ставлю noEmit, бо запускає Playwright, і налаштовую paths для коротких імпортів. Перевірку типів запускаю окремо в CI через tsc --noEmit.

Типи середовища, .d.ts і змінні оточення

Файли .d.ts містять тільки оголошення типів, без коду. Їх використовують для бібліотек на JS і для розширення існуючих типів.

Типова задача - типізувати process.env. За замовчуванням кожна змінна має тип string | undefined, і це правильно: змінної може не бути.

// src/env.ts
function required(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing env variable ${name}`);
  return value;
}

export const env = {
  baseUrl: required('BASE_URL'),
  apiToken: required('API_TOKEN'),
  timeoutMs: Number(process.env.TIMEOUT_MS ?? '30000'),
} as const;

Я віддаю перевагу такому модулю, а не глобальному розширенню типу ProcessEnv через declaration merging. Розширення типу лише каже компілятору "змінна є", але не перевіряє цього в рантаймі. Модуль з перевіркою падає одразу з зрозумілою помилкою, а не посеред тесту з undefined в URL. Ще краще - валідувати змінні схемою.

Пакети @types/... містять типи для JS-бібліотек, наприклад @types/node. Опція types у tsconfig обмежує, які глобальні типи підключаються автоматично. Якщо в одному репозиторії є Playwright і Jest, глобальні expect можуть конфліктувати, і тоді налаштовують окремі tsconfig для різних частин.

Ще один інструмент - project references і кілька tsconfig: базовий tsconfig.base.json і окремі конфіги для e2e, API-тестів і утиліт через extends.

На співбесіді: .d.ts - це файли тільки з типами. Змінні оточення я не просто типізую, а читаю через модуль з перевіркою, щоб тест падав одразу з зрозумілою помилкою, якщо змінної немає. Для різних частин репозиторію використовую окремі tsconfig через extends.