Передать { productId: 123 } в метод который ожидает { id: number; name: string; price: number }: ошибка молча проглатывается в рантайме. С интерфейсом TypeScript поймает неверное имя поля прямо в редакторе, до запуска теста. Изменил интерфейс, и все вызовы которые не совпадают сразу подсвечиваются. Статья разбирает интерфейсы для тестовых данных, сигнатур методов Page Object, API-ответов и типизированных фикстур, плюс разницу между type и interface, и дженерик-паттерны для переиспользуемых форм вроде пагинированных ответов.
Зачем TypeScript в POM
Без типов:
class ProductPage {
async addToCart(product) { // product — что угодно
await this.page.click(`[data-id="${product.id}"]`);
}
}
// На месте вызова нет подсказок, легко передать не то
await productPage.addToCart({ productId: 123 }); // опечатка в имени поляС интерфейсом:
interface Product {
id: number;
name: string;
price: number;
}
class ProductPage {
async addToCart(product: Product) {
await this.page.click(`[data-id="${product.id}"]`);
}
}
// Редактор поймает сразу
await productPage.addToCart({ productId: 123 }); // Error: productId не существует в Product
await productPage.addToCart({ id: 123, name: 'Laptop', price: 999 }); // корректноБазовый синтаксис интерфейса
Интерфейс описывает форму объекта:
interface User {
id: number;
email: string;
role: 'admin' | 'member' | 'viewer';
isActive: boolean;
createdAt?: string; // Необязательное поле (знак ?)
}Обязательные поля должны присутствовать всегда. Необязательные поля (?) могут отсутствовать. Union-типы ('admin' | 'member') разрешают только эти конкретные значения.
Интерфейсы для тестовых данных
Самое частое применение: типизированные объекты с тестовыми данными.
// data/users.ts
export interface UserCredentials {
email: string;
password: string;
}
export interface UserProfile extends UserCredentials {
name: string;
role: 'admin' | 'member';
}
export const TEST_USERS = {
admin: {
email: 'admin@test.com',
password: 'AdminPass1',
name: 'Test Admin',
role: 'admin' as const,
} satisfies UserProfile,
member: {
email: 'member@test.com',
password: 'MemberPass1',
name: 'Test Member',
role: 'member' as const,
} satisfies UserProfile,
};Ключевое слово satisfies (TypeScript 4.9+) проверяет что объект соответствует интерфейсу, но сохраняет литеральные типы.
Интерфейсы для Page Object
// pages/types.ts
import { Page, Locator } from '@playwright/test';
export interface PageObject {
page: Page;
navigate(): Promise<void>;
}
export interface LoginPageInterface extends PageObject {
emailInput: Locator;
passwordInput: Locator;
submitButton: Locator;
errorMessage: Locator;
login(email: string, password: string): Promise<void>;
}// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
import { LoginPageInterface } from './types';
export class LoginPage implements LoginPageInterface {
page: Page;
emailInput: Locator;
passwordInput: Locator;
submitButton: Locator;
errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByTestId('email-input');
this.passwordInput = page.getByTestId('password-input');
this.submitButton = page.getByTestId('submit-btn');
this.errorMessage = page.getByTestId('error-message');
}
async navigate(): Promise<void> {
await this.page.goto('/login');
}
async login(email: string, password: string): Promise<void> {
await this.navigate();
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}Ключевое слово implements говорит TypeScript: «этот класс обязан реализовать всё что требует интерфейс». Забыл метод, получаешь ошибку компиляции.
type vs interface
Оба описывают форму объекта. Практические отличия:
| | interface | type |
|-|-------------|--------|
| Расширение | ключевое слово extends | пересечение через & |
| Declaration merging | Да (поля можно дописывать в нескольких местах) | Нет |
| Union-типы | Нет | Да |
| Для объектов | предпочтительно | тоже работает |
Правило простое: interface для форм объектов (особенно Page Object и модели данных), type для union-ов, примитивов и сложных комбинаций.
// interface: хорошо подходит для форм объектов
interface ProductFilter {
category?: string;
minPrice?: number;
maxPrice?: number;
inStock?: boolean;
}
// type: нужен для union-ов
type TestEnvironment = 'local' | 'staging' | 'production';
type Callback = () => void | Promise<void>;
type UserOrAdmin = User | Admin;Дженерик-интерфейсы
Дженерики позволяют писать гибкие интерфейсы:
// Пагинированный API-ответ для любого типа данных
interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
// Использование с конкретными типами
type UsersResponse = PaginatedResponse<User>;
type ProductsResponse = PaginatedResponse<Product>;
type OrdersResponse = PaginatedResponse<Order>;// В тесте
const response = await request.get('/api/users?page=1&limit=10');
const body: UsersResponse = await response.json();
expect(body.data).toHaveLength(10);
expect(body.totalPages).toBeGreaterThan(0);
// TypeScript знает что body.data[0] — это User
expect(body.data[0].email).toBeTruthy();Интерфейсы для API-ответов
Типизируй API-ответы: получишь автодополнение и защиту от опечаток в именах полей.
// types/api.ts
export interface LoginResponse {
token: string;
expiresAt: string;
user: {
id: number;
email: string;
role: string;
};
}
export interface ErrorResponse {
error: string;
message: string;
field?: string; // Присутствует при ошибках валидации
}
export interface CreateUserRequest {
email: string;
password: string;
name?: string;
role?: 'admin' | 'member';
}// В тестах
test('login returns correct response', async ({ request }) => {
const response = await request.post('/api/auth/login', {
data: { email: 'user@test.com', password: 'ValidPass1' },
});
const body: LoginResponse = await response.json();
// TypeScript знает точно что есть на объекте body
expect(body.token).toBeTruthy();
expect(body.user.id).toBeGreaterThan(0);
expect(body.user.role).toBe('member');
});Наследование интерфейсов
extends позволяет строить на основе существующих интерфейсов:
interface BaseEntity {
id: number;
createdAt: string;
updatedAt: string;
}
interface User extends BaseEntity {
email: string;
name: string;
role: 'admin' | 'member';
}
interface Product extends BaseEntity {
name: string;
price: number;
category: string;
inStock: boolean;
}User и Product оба получают id, createdAt, updatedAt из BaseEntity, плюс собственные поля.
Интерфейсы для фикстур
Типизируй кастомные фикстуры Playwright:
// fixtures/types.ts
import { Page } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
export interface AppFixtures {
loginPage: LoginPage;
dashboardPage: DashboardPage;
authenticatedPage: Page;
}
export interface TestUser {
id: number;
email: string;
token: string;
role: 'admin' | 'member';
}
export interface ApiFixtures {
testUser: TestUser;
adminToken: string;
}// fixtures/index.ts
import { test as base } from '@playwright/test';
import { AppFixtures, ApiFixtures } from './types';
export const test = base.extend<AppFixtures & ApiFixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
// ...
});tsconfig.json для Playwright
Включи строгий режим для полноценной проверки типов:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"outDir": "./dist",
"baseUrl": ".",
"paths": {
"@fixtures": ["./fixtures/index.ts"],
"@pages/*": ["./pages/*"],
"@data/*": ["./data/*"]
}
},
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}Конфигурация paths позволяет использовать алиасы при импорте:
import { test } from '@fixtures'; // вместо '../../fixtures/index'
import { LoginPage } from '@pages/LoginPage'; // вместо '../../pages/LoginPage'Итог
interfaceдля Page Object-ов, тестовых данных и форм API-ответовtypeдля union-типов и сложных комбинаций типовextendsдля построения на основе существующих интерфейсов (принцип DRY)- Дженерик-интерфейсы (
PaginatedResponse) для переиспользуемых форм - Типизируй API-ответы, чтобы TypeScript помогал верифицировать поля
implementsв классах Page Object закрепляет контракт
TypeScript в автоматизации тестов: не про церемонии. Про то чтобы редактор поймал product.productId вместо product.id до того как потратишь 10 минут отлаживая почему клик не срабатывает.