Skip to main content

Why TypeScript for Testing?

TypeScript brings significant benefits to test automation:

Type Safety

Catch errors before runtime with static type checking

IntelliSense

Better autocomplete and documentation in your IDE

Refactoring

Safely rename and restructure code with confidence

Documentation

Types serve as inline documentation for your code

Basic TypeScript in Tests

Type Annotations

Add type annotations to variables:
// ❌ JavaScript - no type safety
let loginPage = new LoginPage()
let username = 'standard_user'

// ✅ TypeScript - with types
let loginPage: LoginPage = new LoginPage()
let username: string = 'standard_user'

Fixture Types

Type your fixture data:
// Define fixture structure
interface UserCredentials {
  standard: string
  locked: string
  problem: string
  performance: string
  password: string
}

// Use with type safety
cy.fixture<UserCredentials>('users').then((user) => {
  // TypeScript knows user.standard exists
  loginPage.typeUsername(user.standard)
  loginPage.typePassword(user.password)
})

API Response Types

interface Booking {
  firstname: string
  lastname: string
  totalprice: number
  depositpaid: boolean
  bookingdates: {
    checkin: string
    checkout: string
  }
  additionalneeds?: string
}

it('Should return valid booking', () => {
  cy.request<Booking>('GET', '/booking/1').then((response) => {
    // TypeScript knows the response structure
    expect(response.body.firstname).to.be.a('string')
    expect(response.body.totalprice).to.be.a('number')
  })
})

Page Object TypeScript Patterns

Proper Access Modifiers

export default class LoginPage extends BasePage {
  // Private - only accessible within this class
  private usernameField: string
  private passwordField: string
  
  // Public - accessible from tests
  public typeUsername(username: string): void {
    cy.get(this.usernameField).type(username)
  }
  
  // Protected - accessible by subclasses
  protected getFieldValue(selector: string): Cypress.Chainable<string> {
    return cy.get(selector).invoke('val')
  }
}

Return Type Annotations

class LoginPage {
  // Void return type - method performs action
  public clickOnLoginButton(): void {
    cy.get(this.loginBtnField).click()
  }
  
  // Chainable return type - method returns Cypress command
  public getErrorMessage(): Cypress.Chainable<string> {
    return cy.get(this.errorMsgAlert).invoke('text')
  }
  
  // This return type - enables method chaining
  public typeUsername(username: string): this {
    cy.get(this.usernameField).type(username)
    return this
  }
}

Method Chaining with ‘this’

class LoginPage {
  public typeUsername(username: string): this {
    cy.get(this.usernameField).type(username)
    return this
  }

  public typePassword(password: string): this {
    cy.get(this.passwordField).type(password)
    return this
  }

  public clickOnLoginButton(): this {
    cy.get(this.loginBtnField).click()
    return this
  }
}

// Usage - fluent interface
loginPage
  .typeUsername('user')
  .typePassword('pass')
  .clickOnLoginButton()

Interface Definitions

User Data Interface

types/users.ts
export interface UserCredentials {
  standard: string
  locked: string
  problem: string
  performance: string
  error: string
  visual: string
  password: string
}

Booking Data Interface

types/booking.ts
export interface BookingDates {
  checkin: string
  checkout: string
}

export interface Booking {
  firstname: string
  lastname: string
  totalprice: number
  depositpaid: boolean
  bookingdates: BookingDates
  additionalneeds?: string  // Optional property
}

export interface BookingResponse {
  bookingid: number
  booking: Booking
}

Using Interfaces in Tests

import { UserCredentials } from '../types/users'
import { Booking } from '../types/booking'

it('Should create booking', () => {
  cy.fixture<UserCredentials>('users').then((user) => {
    cy.fixture<Booking[]>('bookings').then((bookings) => {
      const newBooking = bookings[0]
      
      cy.request<BookingResponse>({
        method: 'POST',
        url: '/booking',
        body: newBooking
      }).then((response) => {
        expect(response.body.booking.firstname).to.equal(newBooking.firstname)
      })
    })
  })
})

Custom Command Types

Define Command Types

cypress/support/commands.ts
declare global {
  namespace Cypress {
    interface Chainable {
      /**
       * Custom command to login via API
       * @param username - User's username
       * @param password - User's password
       * @example cy.loginViaAPI('standard_user', 'secret_sauce')
       */
      loginViaAPI(username: string, password: string): Chainable<void>
      
      /**
       * Custom command to get element by data-test attribute
       * @param selector - data-test attribute value
       * @example cy.getByTestId('login-button')
       */
      getByTestId(selector: string): Chainable<JQuery<HTMLElement>>
    }
  }
}

Cypress.Commands.add('loginViaAPI', (username: string, password: string): void => {
  cy.request({
    method: 'POST',
    url: '/api/login',
    body: { username, password }
  })
})

Cypress.Commands.add('getByTestId', (selector: string) => {
  return cy.get(`[data-test="${selector}"]`)
})

export {}

Using Typed Commands

// TypeScript knows about these commands
cy.loginViaAPI('user', 'pass')  // Autocomplete works!
cy.getByTestId('login-button').click()

Enums for Constants

User Types Enum

types/enums.ts
export enum UserType {
  STANDARD = 'standard_user',
  LOCKED = 'locked_out_user',
  PROBLEM = 'problem_user',
  PERFORMANCE = 'performance_glitch_user',
  ERROR = 'error_user',
  VISUAL = 'visual_user'
}

export enum SortOption {
  NAME_ASC = 'Name (A to Z)',
  NAME_DESC = 'Name (Z to A)',
  PRICE_ASC = 'Price (low to high)',
  PRICE_DESC = 'Price (high to low)'
}

Using Enums

import { UserType, SortOption } from '../types/enums'

it('Should login as standard user', () => {
  loginPage.typeUsername(UserType.STANDARD)
  loginPage.typePassword('secret_sauce')
  loginPage.clickOnLoginButton()
})

it('Should sort products', () => {
  homePage.selectSort(SortOption.PRICE_ASC)
  homePage.verifySortOrder()
})

Generic Types

Generic Page Object

class BasePage<T = any> {
  protected data: T

  constructor(data?: T) {
    this.data = data
  }

  protected getData(): T {
    return this.data
  }
}

class LoginPage extends BasePage<UserCredentials> {
  constructor() {
    super()
  }

  public loadUserData(): void {
    cy.fixture<UserCredentials>('users').then((data) => {
      this.data = data
    })
  }
}

Generic Helper Functions

utils/helpers.ts
export function assertResponse<T>(response: Cypress.Response<T>, expectedStatus: number): void {
  expect(response.status).to.equal(expectedStatus)
  expect(response.body).to.exist
}

export function validateSchema<T>(data: T, requiredKeys: (keyof T)[]): void {
  requiredKeys.forEach(key => {
    expect(data).to.have.property(key)
  })
}

// Usage
it('Should validate booking response', () => {
  cy.request<Booking>('GET', '/booking/1').then((response) => {
    assertResponse(response, 200)
    validateSchema(response.body, ['firstname', 'lastname', 'totalprice'])
  })
})

Type Guards

Runtime Type Checking

utils/typeGuards.ts
export function isBooking(obj: any): obj is Booking {
  return (
    typeof obj.firstname === 'string' &&
    typeof obj.lastname === 'string' &&
    typeof obj.totalprice === 'number' &&
    typeof obj.depositpaid === 'boolean' &&
    obj.bookingdates &&
    typeof obj.bookingdates.checkin === 'string' &&
    typeof obj.bookingdates.checkout === 'string'
  )
}

// Usage
it('Should validate booking structure', () => {
  cy.request('GET', '/booking/1').then((response) => {
    const data = response.body
    
    if (isBooking(data)) {
      // TypeScript knows data is Booking type here
      expect(data.firstname).to.be.a('string')
    } else {
      throw new Error('Invalid booking structure')
    }
  })
})

Utility Types

Pick and Omit

// Pick specific properties
type UserLogin = Pick<UserCredentials, 'standard' | 'password'>

function login(credentials: UserLogin): void {
  loginPage.typeUsername(credentials.standard)
  loginPage.typePassword(credentials.password)
}

// Omit specific properties
type BookingWithoutId = Omit<BookingResponse, 'bookingid'>

function createBooking(booking: BookingWithoutId): void {
  cy.request('POST', '/booking', booking)
}

Partial and Required

// Make all properties optional
type PartialBooking = Partial<Booking>

function updateBooking(id: number, updates: PartialBooking): void {
  cy.request('PATCH', `/booking/${id}`, updates)
}

// Make all properties required
type CompleteBooking = Required<Booking>

function validateBooking(booking: CompleteBooking): void {
  // All properties including additionalneeds are required
  expect(booking.additionalneeds).to.exist
}

Advanced Patterns

Builder Pattern with TypeScript

class BookingBuilder {
  private booking: Partial<Booking> = {}

  public withFirstname(firstname: string): this {
    this.booking.firstname = firstname
    return this
  }

  public withLastname(lastname: string): this {
    this.booking.lastname = lastname
    return this
  }

  public withPrice(totalprice: number): this {
    this.booking.totalprice = totalprice
    return this
  }

  public withDates(checkin: string, checkout: string): this {
    this.booking.bookingdates = { checkin, checkout }
    return this
  }

  public build(): Booking {
    if (!this.booking.firstname || !this.booking.lastname) {
      throw new Error('Firstname and lastname are required')
    }
    return this.booking as Booking
  }
}

// Usage
it('Should create booking with builder', () => {
  const booking = new BookingBuilder()
    .withFirstname('John')
    .withLastname('Doe')
    .withPrice(100)
    .withDates('2024-01-01', '2024-01-10')
    .build()

  cy.request('POST', '/booking', booking)
})

Factory Pattern

utils/factories.ts
export class BookingFactory {
  public static createDefault(): Booking {
    return {
      firstname: 'John',
      lastname: 'Doe',
      totalprice: 100,
      depositpaid: true,
      bookingdates: {
        checkin: '2024-01-01',
        checkout: '2024-01-10'
      }
    }
  }

  public static createWithPrice(price: number): Booking {
    return {
      ...this.createDefault(),
      totalprice: price
    }
  }

  public static createRandom(): Booking {
    return {
      firstname: `User${Math.random().toString(36).substr(2, 9)}`,
      lastname: `Test${Math.random().toString(36).substr(2, 9)}`,
      totalprice: Math.floor(Math.random() * 1000),
      depositpaid: Math.random() > 0.5,
      bookingdates: {
        checkin: '2024-01-01',
        checkout: '2024-01-10'
      }
    }
  }
}

// Usage
it('Should create multiple bookings', () => {
  const booking1 = BookingFactory.createDefault()
  const booking2 = BookingFactory.createWithPrice(500)
  const booking3 = BookingFactory.createRandom()
})

TypeScript Configuration

While the framework doesn’t include a tsconfig.json, you can add one for stricter type checking:
tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020", "DOM"],
    "types": ["cypress", "node"],
    "module": "ESNext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": [
    "cypress/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}

Best Practices

Always specify types for function parameters and return values:
// ✅ Good
public typeUsername(username: string): void {
  cy.get(this.usernameField).type(username)
}

// ❌ Bad - implicit any
public typeUsername(username) {
  cy.get(this.usernameField).type(username)
}
Use specific types instead of any:
// ❌ Bad
cy.fixture('users').then((user: any) => {
  loginPage.typeUsername(user.standard)
})

// ✅ Good
cy.fixture<UserCredentials>('users').then((user) => {
  loginPage.typeUsername(user.standard)
})
Define interfaces for all complex data:
// ✅ Good
interface Booking {
  firstname: string
  lastname: string
  totalprice: number
}

// ❌ Bad - no type definition
const booking = {
  firstname: 'John',
  lastname: 'Doe',
  totalprice: 100
}
Add JSDoc comments for better IDE support:
/**
 * Logs in a user with the provided credentials
 * @param username - The username to login with
 * @param password - The password to login with
 * @throws {Error} If login fails
 */
public userLogin(username: string, password: string): void {
  this.typeUsername(username)
  this.typePassword(password)
  this.clickOnLoginButton()
}

Common TypeScript Errors

Error: Property 'standard' does not exist on type 'unknown'Solution: Add type annotation:
// ❌ Error
cy.fixture('users').then((user) => {
  loginPage.typeUsername(user.standard)  // Error!
})

// ✅ Fixed
cy.fixture<UserCredentials>('users').then((user) => {
  loginPage.typeUsername(user.standard)  // Works!
})
Error: Trying to assign void to a variableSolution: Use proper return types:
// ❌ Error
const result = loginPage.clickOnLoginButton()  // Returns void

// ✅ Fixed - don't assign void methods
loginPage.clickOnLoginButton()
Error: Cannot find name 'Cypress'Solution: Ensure Cypress types are installed:
npm install --save-dev @types/cypress

Next Steps

Page Object Model

Apply TypeScript to page objects

Creating Page Objects

Build type-safe page objects

Maintainability

Use TypeScript for better maintainability

Test Organization

Organize your typed test code

Build docs developers (and LLMs) love