Pasar { productId: 123 } a un método que espera { id: number; name: string; price: number } falla silenciosamente en tiempo de ejecución; con una interfaz, TypeScript detecta el nombre de campo incorrecto en el editor antes de que corra el test. Cambia la interfaz y cada punto de llamada que no coincide muestra un error de inmediato. Este artículo cubre interfaces para datos de prueba, firmas de métodos de Page Object, respuestas de API y fixtures tipados, más la decisión type vs interface y patrones genéricos para formas reutilizables como respuestas paginadas.
Por qué TypeScript en POM
Considera esto sin tipos:
class ProductPage {
async addToCart(product) { // product puede ser cualquier cosa
await this.page.click(`[data-id="${product.id}"]`);
}
}
// Punto de llamada — sin ayuda del editor, fácil equivocarse
await productPage.addToCart({ productId: 123 }); // oops, nombre de campo incorrectoCon una interfaz:
interface Product {
id: number;
name: string;
price: number;
}
class ProductPage {
async addToCart(product: Product) {
await this.page.click(`[data-id="${product.id}"]`);
}
}
// El editor detecta esto de inmediato
await productPage.addToCart({ productId: 123 }); // Error: productId no existe en Product
await productPage.addToCart({ id: 123, name: 'Laptop', price: 999 }); // ✅Sintaxis básica de interfaces
Una interfaz define la forma de un objeto:
interface User {
id: number;
email: string;
role: 'admin' | 'member' | 'viewer';
isActive: boolean;
createdAt?: string; // Campo opcional (el ?)
}Los campos requeridos deben estar siempre presentes. Los campos opcionales (?) pueden estar presentes o no. Los tipos unión ('admin' | 'member') solo permiten esos valores exactos.
Interfaces para datos de prueba
El uso más común: objetos de datos de prueba 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,
};La palabra clave satisfies (TypeScript 4.9+) verifica que el objeto coincida con la interfaz pero preserva los tipos literales.
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();
}
}La palabra clave implements le dice a TypeScript: "esta clase debe tener todo lo que la interfaz requiere". Si olvidas un método, obtienes un error de compilación.
type vs interface
Ambos definen formas de objetos. Las diferencias prácticas:
| | interface | type |
|-|-------------|--------|
| Extensión | palabra clave extends | intersección con & |
| Fusión de declaraciones | Sí (podés añadir campos en múltiples lugares) | No |
| Tipos unión | No | Sí |
| Usar para objetos | ✅ Preferido | ✅ También funciona |
Regla práctica: usainterface para formas de objetos (especialmente page objects y modelos de datos). Usa type para uniones, primitivos y combinaciones complejas.
// interface: excelente para formas de objetos
interface ProductFilter {
category?: string;
minPrice?: number;
maxPrice?: number;
inStock?: boolean;
}
// type: necesario para uniones
type TestEnvironment = 'local' | 'staging' | 'production';
type Callback = () => void | Promise<void>;
type UserOrAdmin = User | Admin;Interfaces genéricas
Los genéricos te permiten escribir interfaces flexibles:
// Una respuesta de API paginada, funciona para cualquier tipo de dato
interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
// Usar con tipos específicos
type UsersResponse = PaginatedResponse<User>;
type ProductsResponse = PaginatedResponse<Product>;
type OrdersResponse = PaginatedResponse<Order>;// En un test
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] es un User
expect(body.data[0].email).toBeTruthy();Interfaces para respuestas de API
Siempre tipa tus respuestas de API para obtener autocompletado y detectar errores de nombre 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 en errores de validación
}
export interface CreateUserRequest {
email: string;
password: string;
name?: string;
role?: 'admin' | 'member';
}// En tests
test('el login devuelve la respuesta correcta', async ({ request }) => {
const response = await request.post('/api/auth/login', {
data: { email: 'user@test.com', password: 'ValidPass1' },
});
const body: LoginResponse = await response.json();
// TypeScript sabe exactamente qué hay en body
expect(body.token).toBeTruthy();
expect(body.user.id).toBeGreaterThan(0);
expect(body.user.role).toBe('member');
});Extender interfaces
Usa 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 y Product tienen id, createdAt, updatedAt de BaseEntity, más sus propios campos.
Interfaces para fixtures
Tipa tus fixtures personalizados de 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 práctico para Playwright
Asegúrate de que tu tsconfig.json tenga modo estricto para una mejor verificación 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"]
}La configuración de paths te permite importar con alias:
import { test } from '@fixtures'; // en lugar de '../../fixtures/index'
import { LoginPage } from '@pages/LoginPage'; // en lugar de '../../pages/LoginPage'Resumen
- Usa
interfacepara page objects, datos de prueba y formas de respuestas de API - Usa
typepara tipos unión y combinaciones de tipos complejas extendspara construir sobre interfaces existentes (principio DRY)- Interfaces genéricas (
PaginatedResponse) para formas reutilizables - Tipá tus respuestas de API para que TypeScript te ayude a verificar los campos
implementsen las clases de page object aplica el contrato
TypeScript en automatización de tests no se trata de ceremonia: se trata de que el editor detecte product.productId cuando quisiste decir product.id, antes de que pases 10 minutos depurando por qué el clic no funciona.