TypeScript & JavaScript
Strong typing, interfaces, generics, and clean architecture patterns for test frameworks.
How I use it
TypeScript is the layer that makes a test framework maintainable at scale. Without it, a renamed API field silently breaks ten step definitions and you only find out in CI. With it, the compiler catches the breakage the moment the interface changes. I use strict mode, explicit return types, and utility types to build test helpers that are hard to misuse.
Typed API response wrapper
A generic response wrapper keeps every API call consistent — status, body, and latency in one typed shape. Step definitions and assertions work against this type rather than raw Axios responses, so if the wrapper changes, TypeScript flags every affected call site immediately.
export interface ApiResponse<T = unknown> {
status: number;
body: T;
latencyMs: number;
}
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
}
export interface ApiError {
code: string;
message: string;
details?: Record<string, string[]>;
}
export function isApiError(body: unknown): body is ApiError {
return (
typeof body === 'object' &&
body !== null &&
'code' in body &&
'message' in body
);
} Generic request helper
A single typed wrapper around Axios gives every test consistent error handling
and latency tracking without repeating the same boilerplate in every step.
The generic parameter T flows through to the return type so
callers get full autocomplete on res.body.
import axios from 'axios';
import type { ApiResponse } from '../types/api';
export async function request<T = unknown>({
method,
url,
body,
headers = {},
}: {
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
url: string;
body?: unknown;
headers?: Record<string, string>;
}): Promise<ApiResponse<T>> {
const start = Date.now();
const res = await axios.request<T>({
method,
url,
data: body,
headers,
validateStatus: () => true,
});
return {
status: res.status,
body: res.data,
latencyMs: Date.now() - start,
};
} Utility types in test factories
Utility types eliminate copy-paste interfaces. CreateBookingPayload
is derived from the full Booking type — so if a required field is
added to the domain model, the factory breaks at compile time rather than
silently producing an invalid request.
interface Booking {
id: string;
bookingRef: string;
customerId: string;
date: string;
time: string;
partySize: number;
tableId: string;
status: 'pending' | 'confirmed' | 'cancelled';
createdAt: string;
}
type CreateBookingPayload = Pick<Booking,
| 'customerId'
| 'date'
| 'time'
| 'partySize'
>;
export function buildBookingPayload(
overrides: Partial<CreateBookingPayload> = {}
): CreateBookingPayload {
return {
customerId: 'cust-test-001',
date: '2026-06-15',
time: '19:30',
partySize: 2,
...overrides,
};
} Typed Page Object with strict locators
Page Object classes declare locators as readonly so nothing
outside the class can reassign them. The constructor is the only place
selectors live — fixing a broken selector is a one-line change with no
risk of missing an occurrence elsewhere.
import type { Page, Locator } from '@playwright/test';
export class BookingPage {
readonly page: Page;
readonly dateInput: Locator;
readonly timeSelect: Locator;
readonly partySizeInput: Locator;
readonly submitButton: Locator;
readonly confirmationBanner: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.dateInput = page.getByLabel('Date');
this.timeSelect = page.getByLabel('Time');
this.partySizeInput = page.getByLabel('Party size');
this.submitButton = page.getByRole('button', { name: 'Book table' });
this.confirmationBanner = page.getByRole('alert', { name: /confirmed/i });
this.errorMessage = page.getByRole('alert', { name: /error/i });
}
async fillBookingForm({
date,
time,
partySize,
}: {
date: string;
time: string;
partySize: number;
}): Promise<void> {
await this.dateInput.fill(date);
await this.timeSelect.selectOption(time);
await this.partySizeInput.fill(String(partySize));
}
async submit(): Promise<void> {
await this.submitButton.click();
}
} What I focus on
- Strict mode everywhere —
"strict": trueintsconfig.jsoncatches null dereferences and implicitanybefore they become runtime failures - Derive types, don't duplicate them —
Pick,Partial,Omit, andRecordkeep payload types in sync with domain interfaces automatically - Explicit return types on helpers — callers see the contract without reading the implementation; TypeScript enforces it doesn't drift
- Type guards over casting —
isApiError(body)narrows safely;body as ApiErrorjust suppresses the error and hides the bug - Readonly on Page Object locators — prevents accidental reassignment and signals these are stable references, not mutable state