Q21 of 48 · Cypress

How do you write a custom command in Cypress with TypeScript types?

CypressMidcypresscustom-commandstypescriptmid

Short answer

Short answer: Register the command with `Cypress.Commands.add(name, fn)` in `cypress/support/commands.ts`, then declare its type in a global `Cypress.Chainable` interface extension. The declaration gives autocomplete and type-checking inside specs.

Detail

Custom commands are how you encapsulate repeated test logic — login, add-to-cart, seed-state — into reusable verbs. The two-step pattern in TypeScript:

Step 1: Implement in cypress/support/commands.ts:

Cypress.Commands.add('loginAs', (role) => {
  cy.session(role, () => {
    cy.request('POST', '/api/login', { role });
  });
});

Step 2: Declare types so TypeScript knows about the command:

declare global {
  namespace Cypress {
    interface Chainable {
      loginAs(role: 'admin' | 'viewer' | 'guest'): Chainable<void>;
    }
  }
}
export {};

The export {} at the bottom makes the file a module so the global declaration is picked up; without it, the declare global is local to the file.

Two flavours of custom commands:

  • Parent commands — start a new chain. Use Cypress.Commands.add('name', fn) with no prevSubject option. cy.loginAs(...).
  • Child commands — operate on a previous subject. Use Cypress.Commands.add('name', { prevSubject: 'element' }, fn). cy.get('input').clearAndType('text').

For a parent command that yields a value (chainable), wrap the return: return cy.wrap(value). The type signature should reflect what's yielded.

Custom commands belong in cypress/support/commands.ts (auto-loaded). Don't put them in spec files — they pollute the global cy and break type checking.

// EXAMPLE

cypress/support/commands.ts

// Implementation
Cypress.Commands.add('loginAs', (role: 'admin' | 'viewer') => {
  cy.session(role, () => {
    cy.request('POST', '/api/login', {
      email: Cypress.env(`${role}Email`),
      password: Cypress.env('password'),
    });
  });
});

Cypress.Commands.add(
  'clearAndType',
  { prevSubject: 'element' },
  (subject: JQuery<HTMLElement>, text: string) => {
    cy.wrap(subject).clear().type(text);
  },
);

// Type declaration
declare global {
  namespace Cypress {
    interface Chainable {
      loginAs(role: 'admin' | 'viewer'): Chainable<void>;
      clearAndType(text: string): Chainable<JQuery<HTMLElement>>;
    }
  }
}
export {};

// WHAT INTERVIEWERS LOOK FOR

Knowing the two-step pattern (add + declare), the `export {}` requirement, and the parent vs child distinction.

// COMMON PITFALL

Forgetting the `declare global` block — the command works at runtime but TypeScript flags every call as 'property does not exist'.