add-cypress-custom-command-in-typescript icon indicating copy to clipboard operation
add-cypress-custom-command-in-typescript copied to clipboard

Custom commands that pass prevSubject don't work with TypeScript

Open shcallaway opened this issue 7 years ago • 4 comments

The following code produces a compilation error: "Expected 1 arguments, but got 0."

// support/commands.ts

function doSomething(subject) {
  console.log(subject);
}

Cypress.Commands.add(
  "doSomething",
  {
    prevSubject: true
  },
  doSomething
);

declare namespace Cypress {
  interface Chainable {
    doSomething: typeof doSomething;
  }
}
// integration/test.ts

it('works', () => {
  cy.get('body').doSomething();
});

shcallaway avatar Jan 04 '18 23:01 shcallaway

When you use typeof, Typescript declares the method using exactly the same interface of itself (in this case, (subject: any) => void).

When you call the method, you pass one less argument and Typescript understands this as a wrong call.

What you need to do is adapt the type of the method in the interface you are declaring, in your case, like this:

declare namespace Cypress {
  interface Chainable {
    doSomething: () => void;
  }
}

Another example:

function doSomethingWithArgs(subject, arg1, arg2) {
  console.log(subject, arg1, arg2);
  return true;
}

Cypress.Commands.add(
  "doSomethingWithArgs",
  {
    prevSubject: true
  },
  doSomethingWithArgs
);

declare namespace Cypress {
  interface Chainable {
    doSomethingWithArgs: (arg1: any, arg2: any) => boolean;
}

That will work!

KoJoVe avatar Nov 29 '18 19:11 KoJoVe

If you have complex types for your args that you don't want to re-type inside the declaration you can do something like this:

type ChainableCommand<T extends (...args: any) => any>
  = T extends (subject: infer I, ...args: infer P) => infer R
    ? (...args: P) => R
    : never;

declare namespace Cypress {
  interface Chainable<Subject> {
    doSomethingWithArgs: ChainableCommand<typeof doSomethingWithArgs>;
  }
}

Cypress.Commands.add('doSomethingWithArgs', { prevSubject: true }, doSomethingWithArgs);

function doSomethingWithArgs(subject: Cypress.Chainable, other: string, args: number) {
  console.log(subject, other, args);
  return subject;
}

Explanation: ChainableCommand maps the type of a function A into the type of a function B that takes the same parameters as A excluding the first one and returns the same thing as A.

aesfer avatar Aug 06 '21 04:08 aesfer

The solutions proposed here work fine with Cypress up to version 8.x but stopped working in Cypress 9. Any idea on how to make it work with the newest version of Cypress without having to resort to brutally overriding the type system by doing the following?

Cypress.Commands.add('myShinyNewCommand', { prevSubject: true }, (myCmd  as unknown) as MyTypeWithoutSubject);

momesana avatar Dec 05 '21 23:12 momesana

@momesana See https://github.com/cypress-io/cypress/pull/19003

Smashman avatar Dec 07 '21 14:12 Smashman