strictNullChecks de TypeScript te obliga a manejar el caso en que el texto de un elemento devuelve null, en vez de dejar que text.length falle en medio del test con Cannot read properties of null. Los métodos de page objects con tipos de retorno explícitos atrapan el otro error frecuente: loginWith() que devuelve Page en vez de DashboardPage hace que los tests no sepan en qué página aterrizaron. Este artículo cubre las configuraciones de tsconfig con mayor impacto en la calidad de los tests, fixtures tipados, unknown en vez de any para datos de respuesta de API, y utility types para separar lo que envías al servidor de lo que recibes de vuelta.
Configuración estricta de tsconfig para tests
Parte de una configuración TypeScript estricta:
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true
},
"include": ["tests/**/*", "fixtures/**/*", "pages/**/*"]
}strictNullChecks es la configuración más valiosa para el código de tests. Te obliga a manejar null y undefined de forma explícita. Esto previene los errores Cannot read properties of null en tiempo de ejecución.
noUnusedLocals y noUnusedParameters eliminan el código muerto que se acumula silenciosamente en los archivos de tests.
Tipar los datos de prueba
Los datos de prueba sin tipos son un error frecuente: confías en que la forma del objeto es correcta al momento de escribir el test, después cambia el nombre de un campo y los tests pasan silenciosamente con datos desactualizados.
// Mal
const user = {
email: 'test@example.com',
password: 'pass123',
};
// Bien: el tipo garantiza que la forma se mantenga correcta
interface TestUser {
email: string;
password: string;
role: 'admin' | 'user' | 'viewer';
}
const testUser: TestUser = {
email: 'test@example.com',
password: 'pass123',
role: 'user',
};Cuando agregas un campo obligatorio a TestUser, TypeScript señala inmediatamente todos los objetos de datos de prueba que no lo incluyen.
Tipar los métodos de page objects
Los métodos de page objects que navegan a una nueva página deben devolver el tipo del nuevo page object:
export class LoginPage {
// El tipo de retorno hace explícita la navegación
async loginWith(email: string, password: string): Promise<DashboardPage> {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
await this.page.waitForURL('/dashboard');
return new DashboardPage(this.page);
}
// void para acciones que permanecen en la misma página
async fillEmail(email: string): Promise<void> {
await this.emailInput.fill(email);
}
// Tipo de retorno para extracción de datos
async getErrorMessage(): Promise<string | null> {
if (await this.errorMessage.isVisible()) {
return this.errorMessage.textContent();
}
return null;
}
}El tipo de retorno Promise obliga a quienes llaman a manejar el caso null. No más const text = await page.getErrorMessage(); expect(text.length).toBeGreaterThan(0) fallando en null.
Fixtures genéricos y tipados
Los fixtures tipados previenen su uso incorrecto:
// fixtures/auth.ts
import { test as base } from '@playwright/test';
interface AuthFixtures {
userPage: Page; // Pre-autenticado como usuario regular
adminPage: Page; // Pre-autenticado como admin
authToken: string;
}
export const test = base.extend<AuthFixtures>({
authToken: async ({ request }, use) => {
const response = await request.post('/api/auth/login', {
data: { email: process.env.TEST_USER_EMAIL!, password: process.env.TEST_USER_PASSWORD! },
});
expect(response.ok()).toBeTruthy();
const { token } = await response.json() as { token: string };
await use(token);
},
userPage: async ({ page, authToken }, use) => {
await page.context().addCookies([{
name: 'auth_token',
value: authToken,
domain: 'localhost',
path: '/',
}]);
await use(page);
},
});TypeScript garantiza que solo puedes acceder a userPage y adminPage en tests que usan esta extensión de fixture, no en tests que importan el test base.
Const assertions para selectores
Evita duplicar strings de selectores en varios tests:
// selectors.ts
export const Selectors = {
login: {
emailInput: 'label:has-text("Email") >> input',
passwordInput: 'label:has-text("Password") >> input',
submitButton: 'button[type="submit"]',
},
checkout: {
cartTotal: '[data-testid="cart-total"]',
placeOrderButton: 'button:has-text("Place order")',
},
} as const; // 'as const' hace que los strings sean tipos literales: sin mutaciones accidentalesMejor aún: usa getByRole, getByLabel, etc. en los page objects en vez de selectores string. Pero cuando los selectores string son necesarios, as const previene reasignaciones accidentales.
Uniones discriminadas para respuestas de API
Cuando pruebas APIs que devuelven formas diferentes según el éxito o el fallo:
type ApiSuccess<T> = {
success: true;
data: T;
};
type ApiError = {
success: false;
error: string;
code: number;
};
type ApiResponse<T> = ApiSuccess<T> | ApiError;
// Type narrowing en tests
const body = await response.json() as ApiResponse<{ orderId: string }>;
if (body.success) {
expect(body.data.orderId).toBeTruthy(); // TypeScript sabe que data existe aquí
} else {
expect(body.code).toBe(422); // TypeScript sabe que error y code existen aquí
}Evitá any: usá unknown para datos sin tipo
Cuando recibes datos de una API y todavía no tienes un tipo definido, usa unknown en vez de any:
// Mal: any desactiva toda verificación de tipos
const body: any = await response.json();
body.nonExistentField.deeply.nested; // Sin error, silenciosamente incorrecto
// Bien: unknown te obliga a validar antes de usar
const body: unknown = await response.json();
// Hay que validar antes de acceder
if (typeof body === 'object' && body !== null && 'orderId' in body) {
console.log((body as { orderId: string }).orderId);
}
// Mejor aún: usá un type guard
function isOrderResponse(data: unknown): data is { orderId: string; status: string } {
return typeof data === 'object' && data !== null && 'orderId' in data;
}
if (isOrderResponse(body)) {
expect(body.orderId).toBeTruthy(); // Tipado
}Utility types para datos de prueba
Los utility types integrados de TypeScript reducen la duplicación en los tipos de datos de prueba:
interface User {
id: string;
email: string;
password: string;
role: 'admin' | 'user';
createdAt: Date;
}
// Input para crear: sin id ni createdAt (los genera el servidor)
type CreateUserInput = Omit<User, 'id' | 'createdAt'>;
// Input para actualizar: todos los campos opcionales
type UpdateUserInput = Partial<Pick<User, 'email' | 'role'>>;
// Objeto para asserts: solo los campos verificables
type UserAssertion = Pick<User, 'email' | 'role'>;Estos tipos hacen explícito el manejo de datos en los tests: lo que envías al servidor y lo que recibes de vuelta tienen formas diferentes, y TypeScript hace cumplir esa diferencia.
→ See also: Interfaces y Tipos de TypeScript para Page Object Model | Tipos, Interfaces y Genéricos en TypeScript para Fixtures de Tests | Page Object Model en Playwright: De Caótico a Mantenible