Advanced Custom Command Patterns — Overwriting, Chaining and TypeScript Typing

Basic custom commands cover the most common needs, but Cypress offers advanced patterns that take your command library to production quality: overwriting built-in commands (add logging to every cy.visit()), creating dual commands that work as both parent and child chains, adding TypeScript type definitions for IntelliSense, and following naming conventions that keep your command library discoverable and collision-free.

Advanced Custom Command Patterns

These patterns are what separate a handful of convenience commands from a professional, well-typed command library.

// ── PATTERN 1: Overwriting built-in commands ──
// Add logging to every cy.visit() call

Cypress.Commands.overwrite('visit', (originalFn, url, options) => {
  Cypress.log({ name: 'visit', message: `Navigating to: ${url}` });
  return originalFn(url, options);
});


// ── PATTERN 2: Dual commands (parent + child chain) ──
// A command that works both as cy.getByTestId('x') and el.getByTestId('x')

Cypress.Commands.add('getByTestId', { prevSubject: 'optional' },
  (subject, testId: string) => {
    const selector = `[data-cy="${testId}"]`;
    if (subject) {
      // Chained from a parent: el.getByTestId('x') — scoped search
      return cy.wrap(subject).find(selector);
    }
    // Called directly: cy.getByTestId('x') — document-wide search
    return cy.get(selector);
  }
);

// Usage:
// cy.getByTestId('submit-btn').click();                // From document root
// cy.get('.form').getByTestId('email-input').type('x'); // Scoped within .form


// ── PATTERN 3: Commands that return values via .then() ──

Cypress.Commands.add('getCartTotal', () => {
  return cy.get('[data-cy="cart-total"]')
    .invoke('text')
    .then((text) => {
      return parseFloat(text.replace(/[^0-9.]/g, ''));
    });
});

// Usage:
// cy.getCartTotal().should('eq', 59.97);
// cy.getCartTotal().then((total) => { expect(total).to.be.greaterThan(0); });


// ── PATTERN 4: TypeScript type definitions ──
// File: cypress/support/index.d.ts

/*
declare namespace Cypress {
  interface Chainable {
    loginUI(username: string, password: string): Chainable;
    loginAPI(username: string, password: string): Chainable;
    addToCart(productIndex?: number): Chainable;
    cartShouldHave(count: number): Chainable;
    fillAddress(address: {
      firstName: string;
      lastName: string;
      postcode: string;
    }): Chainable;
    getByTestId(testId: string): Chainable>;
    getCartTotal(): Chainable;
  }
}
*/

// With these type definitions:
// - VS Code shows autocomplete for cy.loginUI(), cy.addToCart(), etc.
// - TypeScript catches typos: cy.lognUI() → compile error
// - Parameter types are enforced: cy.addToCart('wrong') → type error


// ── PATTERN 5: Command naming conventions ──

const NAMING_CONVENTIONS = {
  actions:     "verb-based: cy.loginUI, cy.addToCart, cy.fillAddress, cy.submitForm",
  assertions:  "should-prefix: cy.cartShouldHave, cy.pageShouldShow, cy.urlShouldBe",
  queries:     "get-prefix: cy.getByTestId, cy.getCartTotal, cy.getProductNames",
  prefixed:    "domain-prefix for large suites: cy.authLogin, cy.cartAdd, cy.checkoutFill",
};

// ── PATTERN 6: Conditional commands (check before act) ──

Cypress.Commands.add('dismissCookieBanner', () => {
  cy.get('body').then(($body) => {
    if ($body.find('[data-cy="cookie-accept"]').length > 0) {
      cy.get('[data-cy="cookie-accept"]').click();
      cy.get('[data-cy="cookie-banner"]').should('not.exist');
    }
    // If no banner found, do nothing — no error
  });
});
Note: TypeScript type definitions for custom commands are not optional in a professional Cypress project — they are essential. Without them, cy.loginUI() shows a TypeScript error (“Property ‘loginUI’ does not exist on type ‘cy'”), and developers get no autocomplete or parameter validation. The type definition file (cypress/support/index.d.ts) extends the Cypress.Chainable interface with your custom command signatures. Once added, VS Code provides full IntelliSense: autocomplete, parameter hints, and compile-time type checking.
Tip: The prevSubject: 'optional' option creates dual commands that work both as standalone (cy.getByTestId('x')) and chained (cy.get('.form').getByTestId('x')). This pattern is ideal for query commands like getByTestId that should search the entire document when called directly but scope within a parent when chained. It replaces the need for separate cy.getByTestId() and .findByTestId() commands.
Warning: Overwriting built-in commands (Cypress.Commands.overwrite) is powerful but dangerous. If your overwrite has a bug — for example, forgetting to call originalFnevery cy.visit() in your entire suite breaks. Test overwrites thoroughly before deploying them. Use overwrites sparingly: logging, performance timing, and retry enhancement are good use cases. Changing fundamental behaviour (modifying the URL, adding default options) is risky.

Common Mistakes

Mistake 1 — Not adding TypeScript type definitions for custom commands

❌ Wrong: Custom commands work at runtime but show red squiggly lines in VS Code, no autocomplete, and TypeScript compilation warnings.

✅ Correct: Adding a cypress/support/index.d.ts file that declares all custom commands with their parameter types and return types.

Mistake 2 — Naming custom commands that collide with built-in ones

❌ Wrong: Cypress.Commands.add('check', ...) — shadows the built-in .check() command for checkboxes.

✅ Correct: Cypress.Commands.add('verifyCartCount', ...) or Cypress.Commands.add('cartShouldHave', ...) — unique, descriptive names.

🧠 Test Yourself

You create a custom command cy.getByTestId('submit') and want it to also work as cy.get('.form').getByTestId('email') for scoped search. Which Cypress.Commands.add option enables this?