Configuring Build and Test Tooling

8 min read

TypeScript is installed and your tsconfig.json is ready. The last setup step before migrating files is configuring your test runner to process TypeScript. Cypress, Playwright, and Jest each handle this differently. This lesson covers all three, plus ESLint and path aliases — and explains the relationship between your tsconfig.json and the test runner's own compilation pipeline.

How test runners use TypeScript

Most test runners do not use tsc directly to run your tests. They use their own transpiler under the hood:

  • Cypress uses esbuild (Cypress 10+) — fast transpilation, no type checking.
  • Playwright uses esbuild (via @playwright/test) — fast transpilation, no type checking.
  • Jest uses either Babel (@babel/preset-typescript) or ts-jest — Babel transpiles without checking, ts-jest checks.

This means: running your tests does not guarantee type correctness. Type errors in test files will not fail a cypress run or npx playwright test unless you run npm run type-check separately as a CI step.

The CI pipeline you're building should run both:

npm run type-check    # TypeScript checks — catches type errors
npx playwright test   # Test runner — runs the actual tests

Cypress

Cypress 10+ supports TypeScript natively. If you rename a .cy.js file to .cy.ts, Cypress picks it up automatically — no additional configuration for basic usage.

For correct IDE support and custom type declarations (like the custom commands from TypeScript for QA — Cypress), add a cypress/tsconfig.json:

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "types": ["cypress", "node"],
    "lib": ["ES2022", "DOM"]
  },
  "include": ["**/*.ts", "**/*.js"]
}

"types": ["cypress", "node"] restricts auto-loaded type packages for the Cypress directory. Without it, TypeScript loads all installed @types packages — which can cause conflicts between Cypress's DOM types and Node types.

The custom commands file (cypress/support/commands.ts) needs the interface augmentation you saw in the TypeScript for QA course:

// cypress/support/commands.ts
declare global {
  namespace Cypress {
    interface Chainable {
      login(email: string, password: string): Chainable<void>;
      seedUser(role: "admin" | "member"): Chainable<string>;
    }
  }
}
 
Cypress.Commands.add("login", (email, password) => {
  cy.request("POST", "/api/auth/login", { email, password })
    .its("body.token")
    .then((token) => cy.setCookie("auth_token", token));
});

Playwright

Playwright is TypeScript-first. Its generated projects use TypeScript by default. Migrating a JavaScript Playwright project is straightforward:

  1. Rename playwright.config.jsplaywright.config.ts
  2. Update imports in the config to use ES module syntax
  3. Rename test files from .spec.js to .spec.ts one at a time

The Playwright config in TypeScript:

// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
 
export default defineConfig({
  testDir: "./tests",
  timeout: 30_000,
  use: {
    baseURL: process.env.BASE_URL ?? "http://localhost:3000",
    trace: "on-first-retry",
  },
  projects: [
    { name: "chromium", use: { ...devices["Desktop Chrome"] } },
  ],
});

For custom fixtures, TypeScript unlocks the full fixture typing pattern from the TypeScript with Playwright lesson:

// fixtures.ts
import { test as base } from "@playwright/test";
import { LoginPage } from "./pages/LoginPage";
 
type Fixtures = { loginPage: LoginPage };
 
export const test = base.extend<Fixtures>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
});

Jest with ts-jest

Jest's default transformer doesn't understand TypeScript. You have two options:

Option A — ts-jest (type-checks during test runs):

npm install --save-dev ts-jest @types/jest
// jest.config.js
module.exports = {
  preset: "ts-jest",
  testEnvironment: "node",
  testMatch: ["**/*.test.ts"],
};

Option B — Babel preset (transpiles without type checking, faster):

npm install --save-dev @babel/preset-typescript @babel/preset-env @babel/core babel-jest
// babel.config.json
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-typescript"
  ]
}

For migration purposes, ts-jest is preferable: it uses your tsconfig.json and gives you real type errors in Jest test files. The Babel option is faster but provides no type safety — you still need to run npm run type-check separately.

ESLint with TypeScript

TypeScript catches type errors; ESLint enforces code style and catches logical issues that TypeScript cannot. Add both:

npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint
// .eslintrc.js
module.exports = {
  parser: "@typescript-eslint/parser",
  parserOptions: { project: "./tsconfig.json" },
  plugins: ["@typescript-eslint"],
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
  ],
  rules: {
    "@typescript-eslint/no-explicit-any": "warn",       // encourage removing any
    "@typescript-eslint/no-unused-vars": "error",        // catch dead code
    "@typescript-eslint/no-floating-promises": "error",  // catches missing await
  },
};

no-floating-promises is particularly valuable in Playwright and Cypress tests — it catches page.click(selector) without await, which causes tests to pass for the wrong reasons.

Path aliases

As your project grows, relative imports become hard to read:

import { LoginPage } from "../../../pages/LoginPage";

Path aliases map shorthand names to directories:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@pages/*": ["./cypress/pages/*"],
      "@fixtures/*": ["./cypress/fixtures/*"],
      "@support/*": ["./cypress/support/*"]
    }
  }
}
import { LoginPage } from "@pages/LoginPage"; // much cleaner

Path aliases in tsconfig.json are honoured by VS Code and tsc, but not automatically by Jest or Node.js. For Jest, add moduleNameMapper to jest.config.js:

module.exports = {
  moduleNameMapper: {
    "^@pages/(.*)$": "<rootDir>/cypress/pages/$1",
    "^@fixtures/(.*)$": "<rootDir>/cypress/fixtures/$1",
  },
};

⚠️ Common mistakes

  • Assuming cypress run catches type errors. It doesn't. Cypress uses esbuild, which strips types without checking them. A test file with a type error will run and may pass — the error is invisible until you run tsc --noEmit. Always include type-check as a separate CI step before your test run.
  • Configuring Mocha without ts-node/register. If you're using Mocha (common in API test projects), it doesn't understand TypeScript by default. Add "require": ["ts-node/register"] to .mocharc.json and "extensions": ["ts"] to pick up .ts test files.
  • Setting isolatedModules: true without reading the implications. Some tools (esbuild, swc) require this flag because they transpile each file independently without global type information. It prohibits certain TypeScript features (const enum, namespace imports). If your setup requires it, be aware of the constraints before you write code that relies on those features.

🎯 Practice task

Configure TypeScript end-to-end for your test runner.

  1. Pick the test runner you use — Cypress, Playwright, or Jest.
  2. Follow the setup for that runner from this lesson. Install any required packages.
  3. Rename one test file from .js to .ts — the simplest test file in the project.
  4. Run the test suite (cypress run, npx playwright test, or npx jest). The renamed test should still pass.
  5. Now run npm run type-check. If there are errors in the renamed file, fix them.
  6. Confirm both commands exit successfully: the type check and the test run.
  7. Stretch: add the no-floating-promises ESLint rule and run npx eslint on your converted test file. If the rule flags anything, it's a real bug — a missing await that was silently being ignored.

This is the end of Chapter 2. Your project now has TypeScript installed, a migration-friendly tsconfig.json, a type-check script, and a configured test runner that can process TypeScript files. You're ready to start migrating files in earnest — Chapter 3 covers how.

// tip to track lessons you complete and pick up where you left off across devices.