Skip to main content
Cypress App

TypeScript Support

info
What you'll learn​
  • How to set up TypeScript in Cypress
  • How to configure TypeScript for custom commands, custom queries, assertions, and plugins
  • How to use TypeScript with Cypress component testing
  • How to avoid clashing types with Jest
  • How to set up your development environment for TypeScript

Cypress ships with official type declarations for TypeScript. This allows you to write your tests in TypeScript.

Why TypeScript with Cypress?​

Writing Cypress tests in TypeScript gives your team a tight feedback loop that catches problems before a single browser opens — and long before a bug reaches production.

Catch errors at the editor, not in CI​

TypeScript surfaces mismatched selectors, wrong argument types, and missing return values as red underlines in your editor the moment you type them. Without TypeScript, those same mistakes surface as a failing test run minutes or hours later — or worse, a bug reported by a user. Moving that feedback earlier eliminates a whole class of debugging cycles.

IntelliSense makes the API self-documenting​

When your test file is TypeScript, your editor can autocomplete every cy.* command, show its accepted parameters, and display the inline JSDoc as you type. Your team spends less time reading reference docs and less time guessing whether the third argument is optional. For custom commands you expose to the rest of the team, typed declarations act as a lightweight contract: the name, parameters, and return type are machine-verified so callers always get correct usage.

Safer refactoring across the codebase​

When a selector utility, a fixture type, or a page-object method changes, the TypeScript compiler points to every test that is now broken — across the entire project — before you run anything. On a large team, this makes refactoring a planned, predictable operation rather than a game of grep-and-hope.

Lower cost of onboarding and review​

New engineers can explore the test suite with editor assistance rather than reading every helper file to understand what it does. Code reviewers can focus on intent rather than hunting for type mismatches. Both effects compound over time: less ramp-up cost, fewer back-and-forth review cycles, and a test suite that stays readable as it grows.

Shared types between app code and tests​

Because your Cypress support files are plain TypeScript, they can import the same type definitions your application uses — API response shapes, domain models, enum values. A type change in the app propagates into the tests automatically, so your tests stay aligned with the code they exercise without manual synchronization.

Get Started​

Install TypeScript​

To use TypeScript with Cypress, you will need TypeScript 5.x or TypeScript 6.x. If you do not already have TypeScript installed as a part of your framework, you will need to install it:

npm install typescript --save-dev

Configure tsconfig.json​

We recommend creating a tsconfig.json inside your cypress folder with the following configuration:

tsconfig.json
{
"compilerOptions": {
"target": "es6",
"lib": ["es6", "dom"],
"sourceMap": true,
"types": ["cypress", "node"]
},
"include": ["**/*.ts"]
}

The "types" will tell the TypeScript compiler to only include type definitions from Cypress. This will address instances where the project also uses @types/chai or @types/jquery. Since Chai and jQuery are namespaces (globals), incompatible versions will cause the package manager (yarn or npm) to nest and include multiple definitions and cause conflicts.

caution

You may have to restart your IDE's TypeScript server if the setup above does not appear to work. For example:

VS Code (within a .ts or .js file):

  • Open the command palette (Mac: cmd+shift+p, Windows: ctrl+shift+p)
  • Type "restart ts" and select the "TypeScript: Restart TS server." option

If that does not work, try restarting the IDE.

Processing your Cypress configuration and plugins​

Cypress supports TypeScript configuration files with .ts, .mts, and .cts extensions. Under the hood, Cypress uses tsx to transpile and run them inside the Cypress runtime without being bound to limitations of Node or other loaders.

Whether a TypeScript config loads as ESM or CommonJS follows the same Node.js module rules as JavaScript configs. Use .mts and .cts the same way you would use .mjs and .cjs — to force ESM or CommonJS regardless of package.json "type".

Align tsconfig.json with your module format​

Cypress does not use compilerOptions.module when deciding how to load your config — that comes from the file extension and nearest package.json "type". Your tsconfig.json should still align so TypeScript and your editor match how Cypress runs the file:

CommonJS — set compilerOptions.module to "commonjs", then either:

  • name the config cypress.config.cts, or
  • use cypress.config.ts with package.json "type" omitted or set to "commonjs"

ESM — set compilerOptions.module to an ESM-compatible option (for example "NodeNext" or "ES2022"), then either:

  • set package.json "type" to "module" and use cypress.config.ts, or
  • name the config cypress.config.mts

import and export syntax in a .ts config does not by itself mean the file loads as ESM. With compilerOptions.module set to "commonjs", TypeScript transpiles those statements to require() and module.exports — so you can use import in a config that Cypress loads as CommonJS. What matters is the alignment between compilerOptions.module, your config file extension, and package.json "type".

Extending TypeScript Support​

Types for Custom Commands​

When adding custom commands to the cy object, you can manually add their types to avoid TypeScript errors.

For example, if you add the command cy.dataCy to cypress/support/commands.ts (which is imported by your support entry file cypress/support/e2e.ts) like this:

cypress/support/commands.ts
Cypress.Commands.add('dataCy', (value) => {
return cy.get(`[data-cy=${value}]`)
})

Then you can add the dataCy command to the global Cypress Chainable interface (so called because commands are chained together).

cypress/support/commands.ts
declare global {
namespace Cypress {
interface Chainable {
/**
* Custom command to select DOM element by data-cy attribute.
* @example cy.dataCy('greeting')
*/
dataCy(value: string): Chainable<JQuery<HTMLElement>>
}
}
}

declare global requires a TypeScript module file​

TypeScript distinguishes between two kinds of files:

  • Module — any file with at least one top-level import or export statement
  • Script (ambient) — a file with no imports or exports

declare global { ... } is only valid inside a module file. If your commands.ts has no imports, TypeScript will raise an error such as: "Augmentations for the global scope can only be directly nested in external modules or ambient module declarations."

The simplest fix is to add export {} at the bottom of the file so TypeScript treats it as a module:

cypress/support/commands.ts
declare global {
namespace Cypress {
interface Chainable {
dataCy(value: string): Chainable<JQuery<HTMLElement>>
}
}
}

export {} // turns this file into a module so declare global is valid

Alternatively, move your type declarations into a standalone external typings file where no import or export is needed.

Conversely, if you use declare namespace Cypress { ... } without the declare global wrapper inside a file that does have imports, TypeScript will not augment the global Cypress namespace — your custom commands will still appear as unknown.

info

A nice detailed JSDoc comment above the method type will be really appreciated by any users of your custom command.

info

Types of all the parameters taken by the implementation callback are inferred automatically based on the declared interface. Thus, in the example above, the value will be of type string implicitly.

In your specs, you can now use the custom command as expected

it('works', () => {
// from your cypress/e2e/spec.cy.ts
cy.visit('/')
// IntelliSense and TS compiler should
// not complain about unknown method
cy.dataCy('greeting')
})

Adding child or dual commands​

When you add a custom command with prevSubject, Cypress will infer the subject type automatically based on the specified prevSubject.

cypress/support/commands.ts
declare global {
namespace Cypress {
interface Chainable {
/**
* Custom command to type a few random words into input elements
* @param count=3
* @example cy.get('input').typeRandomWords()
*/
typeRandomWords(
count?: number,
options?: Partial<TypeOptions>
): Chainable<JQuery<HTMLElement>>
}
}
}
cypress/support/commands.ts
Cypress.Commands.add(
'typeRandomWords',
{ prevSubject: 'element' },
(subject /* :JQuery<HTMLElement> */, count = 3, options?) => {
return cy.wrap(subject).type(generateRandomWords(count), options)
}
)

Overwriting child or dual commands​

When overwriting either built-in or custom commands which make use of prevSubject, you must specify generic parameters to help the type-checker to understand the type of the prevSubject.

cypress/support/commands.ts
interface TypeOptions extends Cypress.TypeOptions {
sensitive: boolean
}

Cypress.Commands.overwrite<'type', 'element'>(
'type',
(originalFn, element, text, options?: Partial<TypeOptions>) => {
if (options && options.sensitive) {
// turn off original log
options.log = false
// create our own log with masked message
Cypress.log({
$el: element,
name: 'type',
message: '*'.repeat(text.length),
})
}

return originalFn(element, text, options)
}
)

As you can see there are generic parameters <'type', 'element'> are used:

  1. The first parameter is the command name, equal to first parameter passed to Cypress.Commands.overwrite.
  2. The second parameter is the type of the prevSubject that is used by the original command. Possible values:
    • 'element' infers it as JQuery<HTMLElement>
    • 'window' infers it as Window
    • 'document' infers it as Document
    • 'optional' infers it as unknown

Examples:​

Types for Custom Queries​

When adding custom queries with Cypress.Commands.addQuery(), you must declare the query on the Chainable interface before registering it. Cypress infers the query callback's argument types from that declaration.

Unlike custom commands, query callbacks receive this as a Cypress.Command (not Mocha.Context), and the callback must return an inner function that performs the query synchronously.

cypress/support/commands.ts
declare global {
namespace Cypress {
interface Chainable {
/**
* Gets the currently focused DOM element.
* @example cy.focused2()
*/
focused2(
options?: Partial<Cypress.Loggable & Cypress.Timeoutable>
): Chainable<JQuery>
}

// Required so this.set('timeout', ...) type-checks in query callbacks
interface EnqueuedCommandAttributes {
timeout?: number
}
}
}

Cypress.Commands.addQuery(
'focused2',
function focused2(
options: Partial<Cypress.Loggable & Cypress.Timeoutable> = {}
) {
const log =
options.log !== false &&
Cypress.log({
...(options.timeout !== undefined && { timeout: options.timeout }),
})

this.set('timeout', options.timeout)

return () => {
let $el = cy.getFocused()
// ...
return $el
}
}
)
info

When passing optional timeout to Cypress.log(), avoid writing { timeout: options.timeout } directly if options.timeout may be undefined. With strict TypeScript settings such as exactOptionalPropertyTypes, explicitly passing undefined is not assignable to an optional property. Use a conditional spread instead, as shown above.

For queries that accept additional options, combine the relevant Cypress option interfaces with Partial<>, matching how built-in queries are typed. For example, a query with timeout and shadow DOM support might use Partial<Cypress.Loggable & Cypress.Timeoutable & Cypress.Shadow>.

To overwrite an existing query, declare the query on Chainable as usual and use Cypress.Commands.overwriteQuery(). The first argument to your callback is the original query function:

cypress/support/commands.ts
declare global {
namespace Cypress {
interface Chainable {
get(
selector: string,
options?: Partial<Cypress.Loggable & Cypress.Timeoutable>
): Chainable<JQuery<HTMLElement>>
}
}
}

Cypress.Commands.overwriteQuery('get', function (originalFn, ...args) {
return originalFn.apply(this, args)
})

Types for custom window properties​

cy.window() yields an AUTWindow object, which is typed as Window & typeof globalThis & Cypress.ApplicationWindow. The Cypress.ApplicationWindow interface is an empty extension point that you can augment to tell TypeScript about properties your application adds to window.

Extend the interface in your support file or a *.d.ts file:

cypress/support/e2e.ts
declare global {
namespace Cypress {
interface ApplicationWindow {
// add the properties your app sets on window
env: {
DISABLE: boolean
}
}
}
}

TypeScript will now recognize those properties inside cy.window() callbacks:

cy.window().then((win) => {
win.env.DISABLE = true // no TypeScript error
})

Types for custom assertions​

If you extend Cypress assertions, you can extend the assertion types to make the TypeScript compiler understand the new methods. See the Recipe: Adding Chai Assertions for instructions.

Types for plugins​

When using defineConfig in your cypress.config.ts file, TypeScript types for setupNodeEvents are inferred automatically — no additional annotation is needed:

cypress.config.ts
import { defineConfig } from 'cypress'

export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
})

If you extract your plugin setup into a separate function or file, you can annotate it with the Cypress.PluginConfig type:

cypress/plugins/index.ts
const setupNodeEvents: Cypress.PluginConfig = (on, config) => {
// implement node event listeners here
}

export default setupNodeEvents

Using an External Typings File​

You might find it easier to organize your types by moving them from the support file into an external declaration (*.d.ts) file. To do so, create a new file, like cypress.d.ts, and cut the types for your custom commands/assertions from the support file and into the new file.

Ambient declaration file (no imports needed)​

A plain .d.ts file with no import or export statements is treated by TypeScript as an ambient script. In this context you can declare the namespace directly — the declare global wrapper is not required:

// No imports — TypeScript treats this as an ambient declaration file.
// Declare the namespace directly (no declare global wrapper needed).
declare namespace Cypress {
interface Chainable {
/**
* Custom command to select DOM element by data-cy attribute.
* @example cy.dataCy('greeting')
*/
dataCy(value: string): Chainable<JQuery<HTMLElement>>
}
}
info

If TypeScript does not resolve Cypress types automatically (for example when the types field in your tsconfig.json does not include "cypress"), add a triple-slash reference at the top of the declaration file:

/// <reference types="cypress" />

File with imports (e.g. cy.mount)​

When the declaration file imports something — such as cy.mount for component testing — the import turns it into a TypeScript module and declare global is required:

import { mount } from 'cypress/react'

// Augment the Cypress namespace to include type definitions for
// your custom command.
// Alternatively, can be defined in cypress/support/component.d.ts
// with a <reference path="./component" /> at the top of your spec.
declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount
}
}
}

You might need to include the *.d.ts in the include options in any tsconfig.json files in your project for TypeScript to pick up the new types:

"include": [
"src",
"./cypress.d.ts"
]
"include": [
"**/*.ts",
"../cypress.d.ts"
]
caution

TypeScript must be able to find the declaration file

If you rename or move a declaration file, verify that its path is covered by the include globs in every tsconfig.json that needs to see it. A common mistake is placing a global.d.ts inside cypress/support/ while the cypress/tsconfig.json only includes **/*.ts — .d.ts files are matched by **/*.ts only if the glob is **/*.{ts,d.ts} or simply **/*. Safest practice: list the file explicitly or use "include": ["**/*.ts", "**/*.d.ts"].

Set up your dev environment​

Please refer to your code editor in TypeScript's Editor Support doc and follow the instructions for your IDE to get TypeScript support and intelligent code completion configured in your developer environment before continuing. TypeScript support is built in for Visual Studio Code, Visual Studio, and WebStorm - all other editors require extra setup.

Clashing types with Jest​

If you are using both Jest and Cypress in the same project, the TypeScript types registered globally by the two test runners can clash. For example, both Jest and Cypress provide the clashing types for the describe and it functions. Both Jest and Expect (bundled inside Cypress) provide the clashing types for the expect assertion, etc.

The recommended solution is to use separate tsconfig.json files for your Jest tests and your Cypress tests.

Step 1: Create (or ensure you have) a cypress/tsconfig.json that explicitly limits types to only what Cypress needs:

cypress/tsconfig.json
{
"compilerOptions": {
"target": "es6",
"lib": ["es6", "dom"],
"sourceMap": true,
"types": ["cypress", "node"]
},
"include": ["**/*.ts"]
}

Step 2: Exclude the cypress folder (and cypress.config.ts) from your root tsconfig.json so that Jest does not pick up Cypress global types:

tsconfig.json
{
"exclude": ["cypress.config.ts", "cypress", "node_modules"]
}

With this setup, Jest uses the root tsconfig.json (without Cypress types) and Cypress uses its own cypress/tsconfig.json (with Cypress types), so the two sets of globals no longer interfere with each other.

History​

VersionChanges
15.14.0Added support for TypeScript 6.0
15.0.0Raised minimum required TypeScript version from 4.0+ to 5.0+ and replaced ts-node with tsx to parse and run the cypress.config.ts file.
13.0.0Raised minimum required TypeScript version from 3.4+ to 4.0+
10.0.0Update guide to cover TypeScript setup for component testing
5.0.0Raised minimum required TypeScript version from 2.9+ to 3.4+
4.4.0Added support for TypeScript without needing your own transpilation through preprocessors.

See also​