Migrating a Cypress Project from JS to TS

9 min read

Cypress 10+ has TypeScript support built in. There's no preprocessor to install, no webpack config to write, and no special Cypress plugin required. Renaming a file from .cy.js to .cy.ts is all it takes for the transpilation side. The remaining work is configuration, type declarations for custom commands, and fixing the type errors that appear as you convert files. This lesson walks through the full migration from start to finish.

Step 1: Install TypeScript

If TypeScript isn't already in the project:

npm install --save-dev typescript @types/node

Run npx tsc --version to confirm it's available.

Step 2: Create the root tsconfig.json

Your root tsconfig.json should use the migration-friendly settings from Chapter 2 — allowJs: true, strict: false — so existing JavaScript continues to compile while you convert files incrementally:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "allowJs": true,
    "checkJs": false,
    "strict": false,
    "noImplicitAny": false,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true
  },
  "include": ["**/*.ts", "**/*.js"],
  "exclude": ["node_modules", "cypress/screenshots", "cypress/videos"]
}

Step 3: Create cypress/tsconfig.json

Cypress looks for a tsconfig.json in its own directory. Create one that extends the root config and restricts type loading to Cypress and Node — this prevents DOM type conflicts:

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "types": ["cypress", "node"],
    "isolatedModules": false,
    "target": "ES2022"
  },
  "include": ["**/*.ts"],
  "exclude": []
}

Step 4: Migrate cypress.config.js

Rename the config file and convert to TypeScript import syntax:

git mv cypress.config.js cypress.config.ts
// cypress.config.ts
import { defineConfig } from 'cypress';
 
export default defineConfig({
  e2e: {
    baseUrl: process.env.BASE_URL ?? 'http://localhost:3000',
    supportFile: 'cypress/support/e2e.ts',
    specPattern: 'cypress/e2e/**/*.cy.ts',
  },
});

Note supportFile: 'cypress/support/e2e.ts' — you're pointing it at the TypeScript version before that file exists. Create it next.

Step 5: Migrate support files

git mv cypress/support/e2e.js cypress/support/e2e.ts
git mv cypress/support/commands.js cypress/support/commands.ts

e2e.ts is typically just imports:

// cypress/support/e2e.ts
import './commands';

commands.ts is where the most work happens. Convert each command implementation and add the type augmentation in the same file or a separate .d.ts:

// cypress/support/commands.ts
 
// Implementations
Cypress.Commands.add('login', (email: string, password: string): void => {
  cy.request({
    method: 'POST',
    url: '/api/auth/login',
    body: { email, password },
  }).then((response) => {
    cy.setCookie('auth_token', (response.body as { token: string }).token);
  });
});
 
Cypress.Commands.add('seedUser', (role: 'admin' | 'member' | 'guest'): Cypress.Chainable<string> => {
  return cy.request('POST', '/api/test/users', { role }).its('body.id');
});
 
// Type augmentation
declare global {
  namespace Cypress {
    interface Chainable {
      login(email: string, password: string): Chainable<void>;
      seedUser(role: 'admin' | 'member' | 'guest'): Chainable<string>;
    }
  }
}

Step 6: Migrate test files one at a time

Pick the simplest test file and rename it:

git mv cypress/e2e/login.cy.js cypress/e2e/login.cy.ts
// cypress/e2e/login.cy.ts
describe('Login', () => {
  beforeEach(() => {
    cy.visit('/login');
  });
 
  it('logs in with valid credentials', () => {
    cy.login('alice@test.com', 'password123');
    cy.url().should('include', '/dashboard');
    cy.get('[data-testid="user-menu"]').should('be.visible');
  });
 
  it('shows error with invalid credentials', () => {
    cy.login('bad@email.com', 'wrongpassword');
    cy.get('[data-testid="error-banner"]')
      .should('be.visible')
      .and('contain', 'Invalid credentials');
  });
});

Run the tests immediately after each rename:

npx cypress run --spec 'cypress/e2e/login.cy.ts'

Cypress-specific TypeScript patterns

Typed fixtures. Use the generic overload to type what cy.fixture() resolves to:

interface UserFixture {
  email: string;
  password: string;
  role: 'admin' | 'member';
}
 
cy.fixture<UserFixture>('users/admin.json').then((user) => {
  cy.login(user.email, user.password);
  // user.email — typed as string
});

Typed cy.task responses. cy.task() returns unknown by default. Use the generic overload:

// In the test:
cy.task<{ id: string }>('createUser', { role: 'admin' }).then((user) => {
  cy.log(`Created user: ${user.id}`); // user.id is typed as string
});

Typed aliases. Aliases resolved from cy.get('@alias') require an explicit cast:

cy.get('@apiResponse').then((response) => {
  const body = response as Cypress.Response<{ token: string }>;
  expect(body.token).to.be.a('string');
});

Common issues and their fixes

cy.task result is unknown. Use the generic form: cy.task<ExpectedType>('taskName').

Custom command not recognised after migration. Check that cypress/support/e2e.ts imports ./commands. If the supportFile path in cypress.config.ts doesn't point to the .ts file, Cypress won't load the commands.

Cannot find module 'cypress' in tsconfig. Ensure "types": ["cypress", "node"] is in cypress/tsconfig.json and that cypress is installed as a dev dependency.

Plugin files. If you have a cypress/plugins/index.js, it's now handled inside cypress.config.ts via setupNodeEvents. If you're migrating from Cypress 9 or below, update the plugin API first, then convert to TypeScript.

⚠️ Common mistakes

  • Using the same tsconfig for Cypress and the rest of the project without the types restriction. Without "types": ["cypress", "node"] in the Cypress tsconfig, TypeScript loads all installed @types packages — which can cause describe, it, and expect to resolve to the wrong type package (Jest vs Mocha vs Cypress).
  • Moving all files to .ts before the support files are migrated. Test files import from cypress/support/commands. If commands.ts isn't migrated yet, TypeScript can't find the custom command types. Migrate support files before test files.
  • Not running tests after each rename. TypeScript can compile correctly while a test logic error is hiding in a coercion or assertion. Run cypress run (or at minimum cypress open) on each renamed file.

🎯 Practice task

Migrate a Cypress project's support layer completely before touching test files.

  1. Install TypeScript and create both tsconfig files.
  2. Migrate cypress.config.jscypress.config.ts.
  3. Migrate e2e.jse2e.ts and commands.jscommands.ts. Add type augmentations for every custom command.
  4. Run npm run type-check from the project root — zero errors.
  5. Run npx cypress run. All tests should still pass, because test files are still .js with allowJs: true.
  6. Rename one test file to .cy.ts. Fix any type errors. Run that test in isolation: npx cypress run --spec 'cypress/e2e/yourtest.cy.ts'.
  7. Stretch: add typed fixtures to one test using cy.fixture<YourInterface>('fixture.json'). Confirm that accessing a non-existent field on the fixture produces a compile error.

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