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.
π 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 missingawaiton a typed value is flagged instantly. - Zero setup: Playwright runs
.tstest 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:
| Concept | Where you use it in Playwright | Guide |
|---|---|---|
| Variables and data types | Test data, URLs, timeouts | Variables, Data Types |
| Conditions and loops | Data-driven tests, retries | Conditions, Loops |
| Functions and async/await | Every test and helper | Functions |
| Arrays | Lists of elements and test cases | Arrays |
| Objects | Test data, config, API payloads | Objects |
| Interfaces and types | Typed data and API responses | Interfaces |
| Classes | Page Object Model | Classes |
| Generics | Fixtures, API helpers | Generics |
π‘ 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:
| Type | What it represents |
|---|---|
Page | A browser tab |
Locator | A way to find element(s) on the page |
BrowserContext | An isolated browser session (cookies, storage) |
APIRequestContext | The client for API tests (request fixture) |
TestInfo | Details about the running test (title, retries, attachments) |
expect | The 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 message | Cause | Fix |
|---|---|---|
| Object is possibly ‘null’ | textContent() can return null | Use ?? '' or assert with toHaveText |
| Property ‘x’ does not exist on type | Typo or missing property in a type | Fix the name or add it to the interface |
| ‘await’ expressions are only allowed within async functions | Missing async | Add async before the function |
| Cannot find module ‘../pages/LoginPage’ | Wrong relative path | Check the path and file name casing |
| Argument of type ‘string or undefined’ | Reading process.env | Provide a default: process.env.BASE_URL ?? 'https://...' |
π Recommended Learning Path
- Learn the TypeScript basics from the table above (1 to 2 weeks).
- Write simple tests with locators and assertions: Locators, Assertions.
- Learn fixtures and annotations.
- Build a framework with the Page Object Model and external test data (JSON, CSV, .env).
- Complete the beginner automation project.
- 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
- π TypeScript Interview Questions for Beginners
- π Playwright Automation Project for Beginners
- π Real-World Enterprise Playwright Framework
- π Playwright with TypeScript Training Notes: the full course roadmap
π¬ 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)”