Q35 of 38 · TypeScript
What is your strategy for migrating a large JavaScript Playwright test suite to TypeScript?
TypeScriptSeniortypescriptmigrationjavascriptplaywrightstrategyci-cd
Short answer
Short answer: Enable `allowJs` + `checkJs` first to get compile errors without renaming files. Prioritize shared code (Page Objects, fixtures, helpers) over individual tests. Convert file-by-file with `strict: false` relaxed per module, tightening to full strict per file after cleaning. Block new `.js` files with ESLint once the team reaches proficiency.
Detail
A large migration is a programme, not a weekend task. The goal is to capture TypeScript's refactoring safety without stalling feature work.
Phase 0 — Assess (1 week):
- Count total files, identify the highest-churn and highest-shared modules
- Measure current test run time (TypeScript compilation adds ~10-30s to CI for a typical suite)
- Identify third-party dependencies that lack
@types— create stub.d.tsfiles upfront
Phase 1 — Type-check without converting:
- Add
tsconfig.jsonwithallowJs: true,checkJs: true,strict: false - Add
// @ts-checkto the highest-churn files - Fix type errors surfaced on these files without renaming
Phase 2 — Convert shared code first (sprints 1-4):
- Rename Page Objects, fixture factories, API helpers, custom matchers to
.ts - These deliver the most intellisense and refactoring value per file converted
- Use
// @ts-nocheckas a temporary escape hatch on files with complex legacy patterns
Phase 3 — Convert tests (ongoing):
- Rename test files sprint-by-sprint — pair programming accelerates TypeScript adoption
- Enable
strict: trueper file as it's cleaned
Phase 4 — Enforce and close:
- ESLint rule to block new
.jsfiles in test directories - CI gate:
tsc --noEmitmust pass with zero errors
// EXAMPLE
// tsconfig.json — Phase 1: check without converting
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"strict": false,
"noImplicitAny": false, // loosen initially
"skipLibCheck": true
},
"include": ["tests/**/*"]
}
// Individual file opt-in (before renaming)
// tests/login.spec.js
// @ts-check
/** @param {import('@playwright/test').Page} page */
async function login(page, username, password) {
await page.fill("#username", username);
}
// Phase 2: after rename to .ts — remove @ts-check, add types
// tests/login.spec.ts
import { type Page } from "@playwright/test";
async function login(page: Page, username: string, password: string) {
await page.fill("#username", username);
}// WHAT INTERVIEWERS LOOK FOR
A phased approach with specific milestones. Starting with `allowJs`/`checkJs` before renaming. Converting shared code before tests. Per-file strictness ratcheting. The ESLint enforcement gate. This tests both TypeScript knowledge and change management thinking.
// COMMON PITFALL
Renaming all files to `.ts` on day one — this produces hundreds of errors simultaneously, stalls the team, and often leads to `any` everywhere just to make it compile, which defeats the migration's purpose.