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.ts files upfront

Phase 1 — Type-check without converting:

  • Add tsconfig.json with allowJs: true, checkJs: true, strict: false
  • Add // @ts-check to 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-nocheck as 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: true per file as it's cleaned

Phase 4 — Enforce and close:

  • ESLint rule to block new .js files in test directories
  • CI gate: tsc --noEmit must 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.