TypeScript for Playwright: Complete Beginner Tutorial (Setup, Concepts and Framework)

This is the bridge between learning TypeScript and building real Playwright automation. You will set up a Playwright TypeScript project, learn exactly which TypeScript concepts matter for testing, and build typed tests, page objects and fixtures step by step.

Advertisement

πŸ“– What You’ll Learn: why TypeScript for Playwright, the concepts you need, project setup, playwright.config.ts, async/await and Promise, Page and Locator types, typed test data, page object classes, generic fixtures, type checking with tsc, and common errors.


🎯 Why Use TypeScript with Playwright?

Playwright supports JavaScript, TypeScript, Python, Java and .NET, but TypeScript is the most popular choice for Playwright projects, and it is what the official documentation uses in its examples. The reasons are practical:

  • Autocomplete everywhere: type page. and your editor lists every method with its parameters.
  • Errors before you run: a typo like page.gotoo() or a missing await on a typed value is flagged instantly.
  • Zero setup: Playwright runs .ts test files directly; you do not need to configure a separate compiler.
  • Safer refactoring: rename a page object method and every usage is updated or highlighted.
  • Better job prospects: “Playwright + TypeScript” is one of the most requested combinations in QA automation job posts.

πŸ—ΊοΈ TypeScript Concepts You Need for Playwright

You do not need to master all of TypeScript before writing tests. This is the practical checklist, in learning order, with a guide for each:

ConceptWhere you use it in PlaywrightGuide
Variables and data typesTest data, URLs, timeoutsVariables, Data Types
Conditions and loopsData-driven tests, retriesConditions, Loops
Functions and async/awaitEvery test and helperFunctions
ArraysLists of elements and test casesArrays
ObjectsTest data, config, API payloadsObjects
Interfaces and typesTyped data and API responsesInterfaces
ClassesPage Object ModelClasses
GenericsFixtures, API helpersGenerics

πŸ’‘ Tip: If you are completely new, learn variables, functions, async/await, arrays and objects first. That is enough to write real Playwright tests. Add interfaces, classes and generics when you start building a framework.


βš™οΈ Step 1: Set Up a Playwright TypeScript Project

You need Node.js (LTS version) and VS Code. Then run one command in an empty folder:

npm init playwright@latest

Choose TypeScript when asked, accept the default tests folder, add a GitHub Actions workflow if you want CI, and let it install the browsers. Then install the official Playwright Test for VS Code extension to run and debug tests from the editor.

The generated project looks like this:

my-project/
β”œβ”€β”€ tests/
β”‚   └── example.spec.ts      # your tests
β”œβ”€β”€ playwright.config.ts     # Playwright settings (typed!)
β”œβ”€β”€ package.json
└── package-lock.json

πŸ› οΈ Step 2: Understand playwright.config.ts

The config file is plain TypeScript. defineConfig gives you autocomplete and type checking for every option.

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  timeout: 30_000,               // max time per test (ms)
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 2 : undefined,
  reporter: [['html', { open: 'never' }]],
  use: {
    baseURL: 'https://www.saucedemo.com',
    trace: 'on-first-retry',     // record a trace when a test is retried
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
  ],
});

πŸ“ Note: Because of the types, a misspelled option such as retires: 2 is underlined in red immediately instead of being silently ignored.


✍️ Step 3: Your First TypeScript Test

// tests/login.spec.ts
import { test, expect } from '@playwright/test';

test('valid user can log in', async ({ page }) => {
  await page.goto('/');                                   // uses baseURL
  await page.getByPlaceholder('Username').fill('standard_user');
  await page.getByPlaceholder('Password').fill('secret_sauce');
  await page.getByRole('button', { name: 'Login' }).click();

  await expect(page).toHaveURL(/inventory/);
  await expect(page.locator('.title')).toHaveText('Products');
});

Run it:

npx playwright test                 # run all tests headless
npx playwright test --ui            # interactive UI mode
npx playwright test login.spec.ts   # run one file
npx playwright show-report          # open the HTML report

⏳ async, await and Promise<T>

Almost every Playwright action talks to the browser, so it returns a Promise. Always put await in front, otherwise the next line runs before the action finishes.

// βœ… correct
await page.getByRole('button', { name: 'Login' }).click();

// ❌ missing await: the test may finish before the click happens
page.getByRole('button', { name: 'Login' }).click();

// Helper functions return Promise<T>
async function getCartCount(page: Page): Promise<number> {
  const text = await page.locator('.shopping_cart_badge').textContent();
  return Number(text ?? 0);
}

⚠️ Common mistake: A missing await is the number one cause of flaky Playwright tests. Enable the ESLint rule @typescript-eslint/no-floating-promises to catch it automatically.


🏷️ The Playwright Types You Will Use Most

Import these from @playwright/test to type your helpers and page objects:

TypeWhat it represents
PageA browser tab
LocatorA way to find element(s) on the page
BrowserContextAn isolated browser session (cookies, storage)
APIRequestContextThe client for API tests (request fixture)
TestInfoDetails about the running test (title, retries, attachments)
expectThe assertion function
import { Page, Locator, expect } from '@playwright/test';

async function addToCart(page: Page, productName: string): Promise<void> {
  const item: Locator = page.locator('.inventory_item').filter({ hasText: productName });
  await item.getByRole('button', { name: 'Add to cart' }).click();
  await expect(page.locator('.shopping_cart_badge')).toBeVisible();
}

πŸ“¦ Typing Test Data

Use interfaces and union types so wrong test data is impossible to write.

// test-data/users.ts
export type Role = 'standard' | 'locked' | 'problem';

export interface TestUser {
  username: string;
  password: string;
  canLogin: boolean;
}

export const USERS: Record<Role, TestUser> = {
  standard: { username: 'standard_user',   password: 'secret_sauce', canLogin: true },
  locked:   { username: 'locked_out_user', password: 'secret_sauce', canLogin: false },
  problem:  { username: 'problem_user',    password: 'secret_sauce', canLogin: true },
};
// tests/users.spec.ts
import { test, expect } from '@playwright/test';
import { USERS } from '../test-data/users';

for (const [role, user] of Object.entries(USERS)) {
  test('login behaviour for ' + role + ' user', async ({ page }) => {
    await page.goto('/');
    await page.getByPlaceholder('Username').fill(user.username);
    await page.getByPlaceholder('Password').fill(user.password);
    await page.getByRole('button', { name: 'Login' }).click();

    if (user.canLogin) {
      await expect(page).toHaveURL(/inventory/);
    } else {
      await expect(page.locator('[data-test="error"]')).toBeVisible();
    }
  });
}

πŸ›οΈ Page Objects with Classes

As tests grow, move locators and actions into classes. This is the Page Object Model:

// pages/InventoryPage.ts
import { Page, Locator, expect } from '@playwright/test';

export class InventoryPage {
  readonly title: Locator;
  readonly cartBadge: Locator;

  constructor(private readonly page: Page) {
    this.title = page.locator('.title');
    this.cartBadge = page.locator('.shopping_cart_badge');
  }

  async addItem(name: string): Promise<void> {
    await this.page.locator('.inventory_item').filter({ hasText: name })
      .getByRole('button', { name: 'Add to cart' }).click();
  }

  async expectCartCount(count: number): Promise<void> {
    await expect(this.cartBadge).toHaveText(String(count));
  }
}

Full walkthrough: Playwright Page Object Model step by step.


🧩 Typed Fixtures with Generics

Custom fixtures inject ready-made page objects into every test. test.extend<T>() is a generic, so each fixture is fully typed.

// fixtures.ts
import { test as base } from '@playwright/test';
import { InventoryPage } from './pages/InventoryPage';

type Pages = { inventoryPage: InventoryPage };

export const test = base.extend<Pages>({
  inventoryPage: async ({ page }, use) => {
    await use(new InventoryPage(page));
  },
});
export { expect } from '@playwright/test';
// tests/cart.spec.ts
import { test } from '../fixtures';

test('add two items to the cart', async ({ page, inventoryPage }) => {
  // log in first (or reuse a saved session with storageState)
  await inventoryPage.addItem('Sauce Labs Backpack');
  await inventoryPage.addItem('Sauce Labs Bike Light');
  await inventoryPage.expectCartCount(2);
});

πŸ” Important: Playwright Does Not Type-Check Your Tests

Playwright transpiles TypeScript so it can run it fast, but it does not report type errors when you run npx playwright test. Your editor shows them, but a CI run can still pass with type errors. Add a type-check step:

npx tsc --noEmit

Add a minimal tsconfig.json if your project does not have one:

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "noEmit": true
  },
  "include": ["tests", "pages", "test-data", "*.ts"]
}

πŸ’‘ Tip: Run npx tsc --noEmit before npx playwright test in your GitHub Actions pipeline so type errors fail the build early.


🐞 Common TypeScript Errors in Playwright (and Fixes)

Error messageCauseFix
Object is possibly ‘null’textContent() can return nullUse ?? '' or assert with toHaveText
Property ‘x’ does not exist on typeTypo or missing property in a typeFix the name or add it to the interface
‘await’ expressions are only allowed within async functionsMissing asyncAdd async before the function
Cannot find module ‘../pages/LoginPage’Wrong relative pathCheck the path and file name casing
Argument of type ‘string or undefined’Reading process.envProvide a default: process.env.BASE_URL ?? 'https://...'

πŸš€ Recommended Learning Path

  1. Learn the TypeScript basics from the table above (1 to 2 weeks).
  2. Write simple tests with locators and assertions: Locators, Assertions.
  3. Learn fixtures and annotations.
  4. Build a framework with the Page Object Model and external test data (JSON, CSV, .env).
  5. Complete the beginner automation project.
  6. Prepare with 200 Playwright + TypeScript interview questions.

❓ FAQ

πŸ€” Do I need to learn TypeScript before Playwright?

You need the basics: variables, functions, async/await, arrays and objects. You can learn interfaces, classes and generics while building your first framework.

πŸ€” Is TypeScript or JavaScript better for Playwright?

TypeScript is recommended. It runs with zero extra setup, gives autocomplete and catches mistakes before tests run. JavaScript works too, but large suites are harder to maintain.

πŸ€” How long does it take to learn TypeScript for Playwright?

Most testers with basic programming knowledge are productive in 2 to 4 weeks of regular practice, following the learning path above.

πŸ€” Does Playwright check TypeScript types when running tests?

No. Playwright only transpiles TypeScript. Run npx tsc --noEmit separately (locally and in CI) to catch type errors.

πŸ€” Which TypeScript version does Playwright need?

Playwright bundles its own TypeScript transpiler, so you do not need to install TypeScript to run tests. Install the typescript package only for tsc type checking and editor tooling.


🎯 Next Steps

🎬 Watch the full TypeScript tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

3 thoughts on “TypeScript for Playwright: Complete Beginner Tutorial (Setup, Concepts and Framework)”

Leave a Comment