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.