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/nodeRun 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.tse2e.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
typesrestriction. Without"types": ["cypress", "node"]in the Cypress tsconfig, TypeScript loads all installed@typespackages — which can causedescribe,it, andexpectto resolve to the wrong type package (Jest vs Mocha vs Cypress). - Moving all files to
.tsbefore the support files are migrated. Test files import fromcypress/support/commands. Ifcommands.tsisn'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 minimumcypress open) on each renamed file.
🎯 Practice task
Migrate a Cypress project's support layer completely before touching test files.
- Install TypeScript and create both tsconfig files.
- Migrate
cypress.config.js→cypress.config.ts. - Migrate
e2e.js→e2e.tsandcommands.js→commands.ts. Add type augmentations for every custom command. - Run
npm run type-checkfrom the project root — zero errors. - Run
npx cypress run. All tests should still pass, because test files are still.jswithallowJs: true. - 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'. - 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.