Six chapters of TypeScript theory; now to put it to work. Cypress ships with TypeScript support out of the box — no plugins, no extra build step. This lesson takes you from an empty folder to a typed Cypress project with a custom cy.login() command, typed fixtures, and full IDE autocomplete on every chainable. Every snippet here works in a real Cypress 13+ project.
What Cypress gives you for free
Cypress includes its own TypeScript declarations as part of the cypress npm package. The moment you npm install cypress, your editor knows about cy.get, cy.visit, cy.intercept, every chainer assertion — and the types of every argument they accept.
You don't need ts-loader, ts-node, Babel, or any third-party plugin. Cypress bundles its own TypeScript compiler internally and runs your .ts test files directly.
Setting up a fresh project
mkdir cypress-ts-project && cd cypress-ts-project
npm init -y
npm install cypress typescript --save-dev
npx cypress openThe first run of npx cypress open launches the Cypress app and offers to scaffold a project. Pick "E2E Testing" → choose your preferred browser → and Cypress drops a starter folder structure into the project:
cypress/
├── e2e/
├── fixtures/
└── support/
├── commands.ts ← typed custom commands live here
└── e2e.ts
cypress.config.ts ← top-level Cypress config (TypeScript)
tsconfig.json ← project-wide TS config
Cypress auto-generates a cypress/tsconfig.json tuned for test files. Open it and you'll see the key bits:
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM"],
"types": ["cypress", "node"],
"moduleResolution": "node"
},
"include": ["**/*.ts"]
}The line that matters most is "types": ["cypress"]. That's what wires the cy global, the Cypress namespace, and every chainable assertion into your editor. Without it, cy.get(...) would be a red squiggle.
Your first typed Cypress test
Drop this into cypress/e2e/login.cy.ts:
describe("Login page", () => {
it("logs in with valid credentials", () => {
cy.visit("/login");
cy.get("[data-testid='email']").type("alice@test.com");
cy.get("[data-testid='password']").type("SecurePass123");
cy.get("[data-testid='submit']").click();
cy.url().should("include", "/dashboard");
});
});Hover over cy.get in VS Code — the tooltip shows the full overload signature: optional generics for the element type, options object, the chainable return type. Type a typo like cy.gett(...) and the compiler refuses it. Pass a number where a selector is expected and the compiler complains. The protection is automatic.
Custom commands — the killer feature for typed Cypress
Every test repeats the same login dance. Move it into a custom command and the tests get cleaner; type the command and callers get autocomplete on cy.login("alice", "pw"). Here's the pattern that powers most typed Cypress projects:
// cypress/support/commands.ts
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable<void>;
}
}
}
Cypress.Commands.add("login", (email: string, password: string) => {
cy.get("[data-testid='email']").type(email);
cy.get("[data-testid='password']").type(password);
cy.get("[data-testid='submit']").click();
});
export {};Two halves to read carefully:
- The
declare globalblock uses interface declaration merging — the killer feature interfaces have overtypealiases. Cypress's officialChainableinterface lives insidenode_modules/cypress; this block addsloginto it from your own file. Now everywhere in your project,cy.login(...)autocompletes as a real method on the chainable. Cypress.Commands.addregisters the runtime implementation. If the implementation's parameters drift from the declared signature, the compiler flags it.
The empty export {}; at the bottom turns the file into a module — needed for declare global to take effect. Without it, the declarations would be local to the file.
Typing fixtures
Cypress's cy.fixture() returns a chainable wrapping the parsed JSON. By default that's typed as unknown. Apply your interface inside the .then() to get the real shape:
interface UserFixture {
name: string;
email: string;
password: string;
}
it("logs in using a fixture", () => {
cy.visit("/login");
cy.fixture<UserFixture>("user.json").then((user) => {
cy.login(user.email, user.password);
// ↑ autocomplete works on user
});
});cy.fixture<UserFixture>("user.json") is the typed form — the generic argument tells Cypress what shape to type the resolved value as. Pair it with the typed cy.login from the previous section and the whole test reads as a clean sequence of typed calls.
Cypress + TypeScript at a glance
Step 1 of 5
Install
npm install cypress typescript — Cypress ships its own TypeScript types in the package, no plugins required.
A small Cypress + TS project, complete
Putting it all together in one file:
// cypress/support/commands.ts
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable<void>;
seedUser(role: "admin" | "tester" | "viewer"): Chainable<void>;
}
}
}
Cypress.Commands.add("login", (email, password) => {
// parameter types inferred from the declared Chainable signature
cy.get("[data-testid='email']").type(email);
cy.get("[data-testid='password']").type(password);
cy.get("[data-testid='submit']").click();
});
Cypress.Commands.add("seedUser", (role) => {
cy.task("db:seedUser", role);
});
export {};// cypress/e2e/admin.cy.ts
interface AdminFixture { email: string; password: string }
describe("Admin dashboard", () => {
it("shows the admin tools after login", () => {
cy.seedUser("admin");
cy.fixture<AdminFixture>("admin.json").then((user) => {
cy.visit("/login");
cy.login(user.email, user.password);
cy.contains("Admin Tools").should("be.visible");
});
});
});Custom commands typed, fixture typed, every selector and assertion type-checked. The Cypress commands cheat sheet has the full list of built-in chainers — every one is fully typed, so autocomplete is the fastest reference.
⚠️ Common mistakes
- Forgetting
declare globaland the emptyexport {}. Withoutdeclare global, yourinterface Chainabledeclaration is scoped to the file and won't merge with Cypress's. Withoutexport {}, the file isn't a module anddeclare globaldoesn't take effect. Both lines are necessary; missing either gives you a "Property 'login' does not exist on type 'Chainable'" error from every test that uses the custom command. - Adding
"types": ["cypress"]only at the project root and not incypress/tsconfig.json. Cypress test files live undercypress/and use the nested tsconfig — that's the one that needs"types": ["cypress"]. Without it, the entirecyglobal is unrecognised. - Relying on
cy.fixture("user.json").then((user) => ...)without a generic. The default type ofuserisunknown(oranyon older versions), which means typos in field accesses sail through. Always pass the generic:cy.fixture<UserFixture>("user.json")— or define a runtime validator if the fixture comes from an external system.
🎯 Practice task
Build a fully typed Cypress project. 30-45 minutes.
- Create
cypress-ts-projectand run the install steps from the lesson. - Run
npx cypress openand let it scaffold the project. Confirmcypress/tsconfig.jsonexists with"types": ["cypress"]. - In
cypress/support/commands.ts, add a typedcy.login(email, password)custom command following the pattern from the lesson. - In
cypress/fixtures/, createuser.jsonwith{ "name": "Alice", "email": "alice@test.com", "password": "test" }. Define an interfaceUserFixturein your test file and type the fixture with the generic. - Write
cypress/e2e/login.cy.tsthat visits/login, callscy.login(...)with the fixture values, and asserts the URL changes. (Use a public app like Cypress's own Kitchen Sink demo if you don't have a target URL handy.) - Trigger every type check. After each, read the error and revert:
- Call
cy.login("alice")(missing password). - Call
cy.login(123, "pw")(number for email). - Read
user.userName(typo) inside the.thencallback.
- Call
- Stretch: add a second custom command
cy.seedUser(role: "admin" | "tester" | "viewer")and use it in a second test. Confirm typos in the role string are rejected at compile time.
The next lesson covers the same ground for Playwright — different framework, similar wins, and a few patterns Cypress doesn't expose.