At Twilio SendGrid, we write most of our frontend web applications, especially new pages and features, with TypeScript and React today for better type checking, maintainability, and documentation of our codebase. When we first started writing Cypress tests over two years ago, most of our page objects, helpers, and spec files were still implemented in JavaScript and we’ve been mostly on Cypress version 3.x.x. More recent tests were already written in TypeScript but we still had a large number of files to convert and migrate over to TypeScript. 

We wanted to reap the benefits of typing out our components, unit tests, and Cypress E2E tests fully. What made the process easier was migrating to a newer version of Cypress to take advantage of the TypeScript out of the box support since Cypress 4.4.0.

If you would like to take a step back and learn more generally how to think about writing E2E tests, feel free to check out this blog post. If you would also like to see a one thousand foot overview of the most common things we’ve used or done before with writing Cypress tests across different environments, you can refer to this blog post before getting started with adding TypeScript to your Cypress tests. This blog post assumes you’re familiar with Cypress tests and have used their API before.

If you’re ready to see how we typed things out, let’s first look at some initial changes like adding TypeScript support to Cypress if you are coming from an older version.

Migrating from Cypress 3.x to >= 4.4.0

For those who have already configured their Cypress infrastructure to work with TypeScript in Cypress versions 3.x.x to 4.4.3, you have most likely experienced some trial and error in setting up the proper Webpack preprocessor configuration in your plugins/index.js.  For the Twilio SendGrid team, there were some gotchas with certain files needing to be JavaScript files in the plugins and support folders and hard-to-debug Cypress errors would surface. Once you get something working it should look something like this.


After upgrading from an earlier version of 3.x.x, we were able to drop our Webpack preprocessor config, replace our plugins/index.js file with an index.ts file, tinker a bit with our Cypress folder’s tsconfig.json, and finally, using TS files in our Cypress folder (like some_page.spec.ts, index.d.ts, or page_object.ts)–no more Webpack preprocessor config and it just worked! We were pleased at how much cleaner and nicer it was to not manage your own Webpack preprocessor config and to have better TypeScript coverage over our files as shown below.

With TypeScript support covered, we then looked at the Cypress team’s example real world application, the cypress-real-world-app repo, in order to learn more about how to type things better. We discovered how to type cy.task(“pluginName”, { … }) and Cypress.env(“someEnvVar”) function calls for better chaining and type intellisense support when updating files. We also dug through their accompanying TypeScript documentation. This taught us how to type things like our cy.login() custom command and how to set up a tsconfig.json configuration file. The example application also has a tsconfig.json for you to reference, which can provide you a great base TypeScript configuration for you to customize to your preferences. We recommend for you to stay up to date with the latest Cypress versions, dive into the official Cypress resources, and experiment with your type definition files.

Typing Custom Commands

We created some global custom commands such as cy.login() to handle logging in through the API so it can be reused throughout all of our spec files. Here is an example of how you can type your own login custom command given user credentials and returning an auth token.

Typing Environment Variables

For dealing with multiple testing environments such as dev and staging, we took advantage of configuring environment variables such as: “testEnv” to hold which environment we are currently testing against such as staging, “apiHost” to hold the backend API host, and other variables. You can type out your Cypress.env() calls to better type out functions and other objects that rely on using those environment variable values like this.

Typing Plugins

We created many cy.task() plugin functions to handle things from API calls, polling for services, and checking for matching emails in test email inboxes. Previously when chaining any of these function calls like cy.task().then((data) => {}), the chained data subject would be typed as any or unknown, which was not great for our TypeScript files. We discovered through the Cypress examples how to better type out the plugins based on the plugin name and the arguments passed through in the function calls. This allowed our TypeScript files to detect what the chained data type would be.

One subtle issue we experienced was that the plugin name and arguments must match exactly to how you typed it in. We found it’s important to hover over the chained .then() type and the cy.task() argument object in your editor to double check the types are matching up correctly. At times, if you were using a chained subject from another Cypress function such as cy.getCookie(“auth_token”).its(“value”).then((token) => { }) or cy.wrap(data).then((data) => {}), you need to type those chained data arguments too before passing it in as a cy.task(..., { token, data }) function argument or else you will still see the cy.task(...).then((data) => { }) data part typed as any or unknown. It’s better for you to be more explicit in a lot of the chained Cypress function types such as .its(“value”).then((token: string) => {}) or cy.wrap(data).then((data: DataType) => {}) before passing them in as part of the cy.task() arguments object to be sure the types are working again.

We created separate plugin Typescript files that would export functions to be used back in our plugins/index.ts. As the number of plugins grows, we recommend for you to organize these plugin function implementations by page or feature to keep your plugins/index.ts file small. You should make it easier to read at a glance where you define all of your cy.task(...) functions in your plugins/index.ts file. You can finally type out these task functions in your index.d.ts in the following way:

Putting it all together in a type declaration file

We placed all of our types for our custom commands, environment variables, and plugins in an index.d.ts file in the support folder. We recommend for you to place all of your Cypress types as well in a main TypeScript definition file to keep things organized. For bypassing missing types in external dependencies used in your Cypress test code, you may also define module files like “some-lib.d.ts”, which would include declare module ‘some-lib’, to workaround the library’s TypeScript warnings. You can even use TypeScript’s import types feature to bring in types/interfaces defined inside your other plugins/utils files to avoid duplicating your type definitions in multiple files. You can add these types within a Cypress namespace and organize them in the following way:

Typing test fixture objects, page objects, and spec files

When we want to load up varying users and metadata for a test environment, we previously illustrated how we can combine environment variables such as “testEnv” with values of “testing” or “staging” in order to extract out the “testing” or “staging” objects from the overall test fixtures object. You can type these test fixture environment objects with generics for a consistent structure for all of your specs to share. For each test environment, you can have the same user credentials and meta fields using a generic type for a test to add as many properties as it needs. See the example below.

Typing out the page objects and typing out the corresponding spec files depend on the Cypress commands, plugins, and other utils you are using. For the most part, typing out the page objects or spec files does not require many changes from their JavaScript counterparts (assuming you typed out the plugins and environment variable calls already). Occasionally, a page object helper function you defined may need some arguments to be typed out or the response coming back from a cy.request() call may need to be typed with as such as response.body as SomeType. Overall, your editor such as VSCode can auto-detect the chained types of your cy.task() or cy.customCommand() calls without you needing to add more types in your spec files to compensate for any TypeScript warnings.

Here is an example of parts of a page object with some helper functions and a spec file that uses the page object, login custom command, and plugin tasks.

Conclusion

Adding TypeScript to our Cypress tests helped us avoid bugs and improved our developer experience when writing Cypress tests. When using cy.task(), Cypress.env(), and cy.customCommand() function calls, we can get better type checking on the function arguments and outputs as well as take advantage of code completion in our IDE such as VSCode.

The key is to create your own type declaration file such as an index.d.ts file in which you can override or extend the “Cypress” and “Chainable” interfaces based on the custom commands, environment variables, and task plugin functions you are using. In your page object and spec TypeScript files, you can then utilize those Cypress functions and hover over or follow the definition of expected input and output types.

Moreover, try using TypeScript with Cypress as it has out of the box support for TypeScript in its more recent versions. Test whether it helps you document functions more clearly and avoid incorrect API usage. Your TypeScript tests will likely still look similar to your JavaScript Cypress tests, so you can steadily convert some tests at a time to compare and contrast the approaches.

If you’re interested in more posts related to what we learned from our Cypress tests, stay tuned for future blog posts related to organizing and consolidating your Cypress tests and integrating these tests with Docker and CICD.



Alfred Lucero
Alfred Lucero is a software engineer for the Mako Frontend team at SendGrid. His favorite part about web development is bringing new designs and ideas to life. He spends his free time exploring Southern California and playing with his corgi, Juno.