Les interfaces TypeScript définissent la forme des données de test et des paramètres de méthodes des page objects. Les erreurs dues à de mauvais arguments, qui apparaissaient à l'exécution, deviennent des erreurs à la compilation que l'éditeur détecte avant même de lancer les tests.

Pourquoi TypeScript dans le POM

Sans types :

class ProductPage {
  async addToCart(product) {  // product peut être n'importe quoi
    await this.page.click(`[data-id="${product.id}"]`);
  }
}

// Aucune aide de l'éditeur, erreur facile à commettre
await productPage.addToCart({ productId: 123 }); // oups, mauvais nom de champ

Avec une interface :

interface Product {
  id: number;
  name: string;
  price: number;
}

class ProductPage {
  async addToCart(product: Product) {
    await this.page.click(`[data-id="${product.id}"]`);
  }
}

// L'éditeur le détecte immédiatement
await productPage.addToCart({ productId: 123 }); // Erreur : productId n'existe pas sur Product
await productPage.addToCart({ id: 123, name: 'Laptop', price: 999 }); // ✅

Syntaxe de base des interfaces

Une interface définit la forme d'un objet :

interface User {
  id: number;
  email: string;
  role: 'admin' | 'member' | 'viewer';
  isActive: boolean;
  createdAt?: string;  // Champ optionnel (le ?)
}

Les champs requis doivent toujours être présents. Les champs optionnels (?) peuvent être présents ou non. Les types union ('admin' | 'member') n'acceptent que ces valeurs exactes.

Interfaces pour les données de test

L'usage le plus courant : typer les objets de données de test.

// 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,
};

Le mot-clé satisfies (TypeScript 4.9+) vérifie que l'objet correspond à l'interface tout en préservant les types littéraux.

Interfaces pour les 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();
  }
}

Le mot-clé implements indique à TypeScript que cette classe doit contenir tout ce que l'interface requiert. Si tu oublies une méthode, tu obtiens une erreur à la compilation.

type vs interface

Les deux définissent la forme d'objets. Les différences pratiques :

| | interface | type |

|---|---|---|

| Extension | mot-clé extends | intersection & |

| Fusion de déclarations | Oui (champs ajoutables en plusieurs endroits) | Non |

| Types union | Non | Oui |

| Pour les objets | ✅ Préféré | ✅ Fonctionne aussi |

Règle pratique : utilise interface pour les formes d'objets (page objects et modèles de données). Utilise type pour les unions, les primitives et les combinaisons complexes.

// interface : idéal pour les formes d'objets
interface ProductFilter {
  category?: string;
  minPrice?: number;
  maxPrice?: number;
  inStock?: boolean;
}

// type : nécessaire pour les unions
type TestEnvironment = 'local' | 'staging' | 'production';
type Callback = () => void | Promise<void>;
type UserOrAdmin = User | Admin;

Interfaces génériques

Les generics permettent d'écrire des interfaces flexibles :

// Une réponse API paginée, compatible avec n'importe quel type de données
interface PaginatedResponse<T> {
  data: T[];
  total: number;
  page: number;
  pageSize: number;
  totalPages: number;
}

// Utilisation avec des types spécifiques
type UsersResponse    = PaginatedResponse<User>;
type ProductsResponse = PaginatedResponse<Product>;
type OrdersResponse   = PaginatedResponse<Order>;

// Dans 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 sait que body.data[0] est un User
expect(body.data[0].email).toBeTruthy();

Interfaces pour les réponses API

Toujours typer les réponses API pour bénéficier de l'autocomplétion et détecter les fautes sur les noms de champs :

// 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;  // Présent pour les erreurs de validation
}

export interface CreateUserRequest {
  email: string;
  password: string;
  name?: string;
  role?: 'admin' | 'member';
}

// Dans les tests
test('la connexion retourne la bonne réponse', async ({ request }) => {
  const response = await request.post('/api/auth/login', {
    data: { email: 'user@test.com', password: 'ValidPass1' },
  });

  const body: LoginResponse = await response.json();

  // TypeScript sait exactement ce que body contient
  expect(body.token).toBeTruthy();
  expect(body.user.id).toBeGreaterThan(0);
  expect(body.user.role).toBe('member');
});

Étendre les interfaces

extends permet de construire sur des interfaces existantes :

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 et Product héritent tous les deux de id, createdAt et updatedAt via BaseEntity, en plus de leurs propres champs.

Interfaces pour les fixtures

Typer les fixtures Playwright personnalisées :

// 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 adapté à Playwright

Active le mode strict pour une meilleure vérification de types :

{
  "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 config paths permet d'importer avec des alias :

import { test } from '@fixtures';              // au lieu de '../../fixtures/index'
import { LoginPage } from '@pages/LoginPage';  // au lieu de '../../pages/LoginPage'

Résumé

  • interface pour les page objects, les données de test et les formes de réponses API
  • type pour les types union et les combinaisons complexes
  • extends pour construire sur des interfaces existantes (principe DRY)
  • Interfaces génériques (PaginatedResponse) pour les formes réutilisables
  • Typer les réponses API pour que TypeScript aide à vérifier les champs
  • implements dans les classes de page objects pour respecter le contrat

TypeScript dans l'automatisation des tests n'est pas une question de cérémonie. C'est l'éditeur qui détecte product.productId quand tu voulais dire product.id, avant que tu passes dix minutes à chercher pourquoi le clic ne fonctionne pas.

→ See also: TypeScript pour QA: Pourquoi les Types Statiques Améliorent Vos Tests | Page Object Model dans Playwright: Du Chaos à la Maintenabilité | Types, Interfaces et Génériques en TypeScript pour les Fixtures de Tests