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 incorrecto

Con 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: usa interface 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 interface para page objects, datos de prueba y formas de respuestas de API
  • Usa type para tipos unión y combinaciones de tipos complejas
  • extends para 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
  • implements en 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.

→ See also: TypeScript para QA: Por Qué los Tipos Estáticos Mejoran tus Tests | Page Object Model en Playwright: De Caótico a Mantenible | Tipos, Interfaces y Genéricos en TypeScript para Fixtures de Tests