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 errado

Com 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: use interface 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 interface para page objects, dados de teste e formas de respostas de API
  • Use type para union types e combinações complexas de tipos
  • extends para 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
  • implements em 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.

→ Veja também: TypeScript para QA: Por que os Tipos Estáticos Melhoram Seus Testes | Page Object Model no Playwright: Do Caos à Manutenibilidade | Tipos, Interfaces e Genéricos em TypeScript para Fixtures de Testes