Passar { productId: 123 } para um método que espera { id: number; name: string; price: number } falha silenciosamente em runtime. Com uma interface, o TypeScript detecta o nome de campo errado no editor antes de o teste rodar. Mude a interface e cada call site que não corresponder mostra um erro imediatamente.
Por que TypeScript no POM
Considere isso sem tipos:
class ProductPage {
async addToCart(product) { // product pode ser qualquer coisa
await this.page.click(`[data-id="${product.id}"]`);
}
}
// Call site: sem ajuda do editor, fácil de passar a coisa errada
await productPage.addToCart({ productId: 123 }); // ops, nome de campo erradoCom uma interface:
interface Product {
id: number;
name: string;
price: number;
}
class ProductPage {
async addToCart(product: Product) {
await this.page.click(`[data-id="${product.id}"]`);
}
}
// O editor detecta isso imediatamente
await productPage.addToCart({ productId: 123 }); // Erro: productId não existe em Product
await productPage.addToCart({ id: 123, name: 'Laptop', price: 999 }); // ✅Sintaxe básica de interface
Uma interface define a forma de um objeto:
interface User {
id: number;
email: string;
role: 'admin' | 'member' | 'viewer';
isActive: boolean;
createdAt?: string; // Campo opcional (o ?)
}- Campos obrigatórios: devem sempre estar presentes
- Campos opcionais (
?): podem ou não estar presentes - Union types (
'admin' | 'member'): apenas esses valores exatos são válidos
Interfaces para dados de teste
O uso mais comum: objetos de dados de teste tipados.
// 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,
};A palavra-chave satisfies (TypeScript 4.9+) verifica que o objeto corresponde à interface mas preserva os tipos literais.
Interfaces para Page Objects
// 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();
}
}A palavra-chave implements diz ao TypeScript: "esta classe deve ter tudo que a interface exige." Se você esquecer um método, recebe um erro de compilação.
type vs interface
Os dois definem formas de objetos. As diferenças práticas:
| | interface | type |
|-|-------------|--------|
| Extensão | palavra-chave extends | interseção com & |
| Declaration merging | Sim (pode adicionar campos em múltiplos lugares) | Não |
| Union types | Não | Sim |
| Uso para objetos | Preferido | Também funciona |
Regra prática: useinterface para formas de objetos (especialmente page objects e modelos de dados). Use type para unions, primitivos e combinações complexas.
// interface: ótimo para formas de objetos
interface ProductFilter {
category?: string;
minPrice?: number;
maxPrice?: number;
inStock?: boolean;
}
// type: necessário para unions
type TestEnvironment = 'local' | 'staging' | 'production';
type Callback = () => void | Promise<void>;
type UserOrAdmin = User | Admin;Interfaces genéricas
Generics permitem escrever interfaces flexíveis:
// Uma resposta de API paginada, funciona para qualquer tipo de dado
interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
// Usar com tipos específicos
type UsersResponse = PaginatedResponse<User>;
type ProductsResponse = PaginatedResponse<Product>;
type OrdersResponse = PaginatedResponse<Order>;// Em um teste
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 sabe que body.data[0] é um User
expect(body.data[0].email).toBeTruthy();Interfaces para respostas de API
Sempre tipifique as respostas de API para ter autocomplete e detectar erros de nome de campo:
// 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; // Presente para erros de validação
}
export interface CreateUserRequest {
email: string;
password: string;
name?: string;
role?: 'admin' | 'member';
}// Em testes
test('login retorna resposta correta', async ({ request }) => {
const response = await request.post('/api/auth/login', {
data: { email: 'user@test.com', password: 'SenhaValida1' },
});
const body: LoginResponse = await response.json();
// TypeScript sabe exatamente o que está em body
expect(body.token).toBeTruthy();
expect(body.user.id).toBeGreaterThan(0);
expect(body.user.role).toBe('member');
});Estendendo interfaces
Use extends para construir sobre interfaces existentes:
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 e Product têm id, createdAt, updatedAt de BaseEntity, mais seus próprios campos.
Interfaces para fixtures
Tipifique suas fixtures customizadas do 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 prático para Playwright
Certifique-se de que seu tsconfig.json tem strict mode para melhor verificação de tipos:
{
"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"]
}A config paths permite importar com aliases:
import { test } from '@fixtures'; // em vez de '../../fixtures/index'
import { LoginPage } from '@pages/LoginPage'; // em vez de '../../pages/LoginPage'Resumo
- Use
interfacepara page objects, dados de teste e formas de respostas de API - Use
typepara union types e combinações complexas de tipos extendspara construir sobre interfaces existentes (princípio DRY)- Interfaces genéricas (
PaginatedResponse) para formas reutilizáveis - Tipifique suas respostas de API para que o TypeScript ajude a verificar campos
implementsem classes de page object impõe o contrato
TypeScript em automação de testes não é burocracia. É o editor detectando product.productId quando você quis dizer product.id, antes de você gastar 10 minutos debugando por que o clique não está funcionando.