Forgetting await before a Playwright assertion produces "Argument of type 'Locator' is not assignable to parameter of type 'string'" at runtime; TypeScript surfaces the same mistake in the editor before the test runs. Playwright compiles TypeScript internally with no separate build step, and the autocomplete it unlocks means you don't need to memorize the API. This article covers the TypeScript additions that matter in test code: type annotations, interfaces for test data, typed Page Objects, and the four errors you'll hit most.
What TypeScript adds to JavaScript
TypeScript is JavaScript with types. Everything you know from JavaScript works in TypeScript. You're adding an annotation layer on top.
``typescript
// JavaScript
function getUser(id) {
return fetchUser(id);
}// TypeScript — same function with types
function getUser(id: number): Promise
return fetchUser(id);
} `
The annotation
: number tells TypeScript that id must be a number. The : Promise tells it what the function returns. If you accidentally pass a string where a number is expected, TypeScript tells you immediately, before the code runs.
Why TypeScript matters for Playwright specifically
Autocomplete. When you type page. in VS Code, TypeScript knows every method that exists on page and shows them to you. You don't need to memorize the API.
Catching missing await. Forget await before an assertion? TypeScript tells you:
`
Argument of type 'Locator' is not assignable to parameter of type 'string'
`
Page Object safety. When you create a Page Object class and call its methods, TypeScript ensures the method exists and you're passing the right types. A typo in a method name is caught immediately.
Playwright's types are excellent. The Playwright team maintains TypeScript type definitions as a first-class part of the project. Autocomplete and type checking work out of the box with no configuration.
Setting up TypeScript for Playwright
When you initialize a Playwright project, choosing TypeScript is one option:
`bash
npm init playwright@latest
`
Choose TypeScript when prompted. This creates:
playwright.config.ts (config file in TypeScript)
tests/example.spec.ts (example test in TypeScript)
tsconfig.json (TypeScript configuration)
If you already have a JavaScript project, you can convert by renaming
.js files to .ts and adding a tsconfig.json.
Basic TypeScript syntax for test code
Type annotations
`typescript
// Primitive types
const email: string = 'admin@becomeqa.com';
const timeout: number = 30000;
const isLoggedIn: boolean = false;
// In practice, TypeScript infers these — you rarely need to write them
const email = 'admin@becomeqa.com'; // TypeScript knows this is a string
`
TypeScript infers types from values. You only need to write type annotations when TypeScript can't figure it out on its own.
Interfaces: defining the shape of objects
`typescript
// Define what a user object looks like
interface User {
email: string;
password: string;
role: 'admin' | 'viewer';
}
// Now TypeScript knows what to expect
const testUser: User = {
email: 'admin@becomeqa.com',
password: 'testpass123',
role: 'admin',
};
// TypeScript will error if you add a field that doesn't exist
const badUser: User = {
email: 'admin@becomeqa.com',
password: 'testpass123',
role: 'admin',
name: 'Alice', // Error: 'name' does not exist in type 'User'
};
`
Interfaces in tests are most useful for test data and Page Object constructor parameters.
Union types
`typescript
// 'admin' | 'viewer' means the value can only be one of these two strings
type UserRole = 'admin' | 'viewer';
// Useful for status fields, types that have a fixed set of values
type ItemStatus = 'Planned' | 'In Progress' | 'Completed';
function selectStatus(status: ItemStatus) {
// TypeScript knows status can only be one of three values
}
selectStatus('Planned'); // ✓ valid
selectStatus('Cancelled'); // ✗ Error: not assignable to type 'ItemStatus'
`
Union types prevent invalid values from getting into your test data.
Optional properties
`typescript
interface TestItem {
destination: string;
status: ItemStatus;
notes?: string; // The ? means this property is optional
}
// Both of these are valid
const item1: TestItem = { destination: 'Tokyo', status: 'Planned' };
const item2: TestItem = { destination: 'Paris', status: 'Completed', notes: 'Visited' };
`
TypeScript in Page Object Model
The real value of TypeScript for QA engineers shows up in Page Objects:
`typescript
import { Page } from '@playwright/test';
// Importing the Page type from Playwright
export class LoginPage {
// TypeScript knows page is a Playwright Page object
constructor(private page: Page) {}
async login(email: string, password: string): Promise {
await this.page.getByRole('button', { name: 'Login' }).click();
await this.page.getByLabel('Username').fill(email);
await this.page.getByLabel('Password').fill(password);
await this.page.getByRole('button', { name: 'Submit' }).click();
}
async getErrorMessage(): Promise {
const error = this.page.getByRole('alert');
if (await error.isVisible()) {
return error.textContent();
}
return null;
}
}
`
When you use this class in a test:
`typescript
test('login with invalid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
// TypeScript knows login() takes two strings
await loginPage.login('wrong@example.com', 'wrongpass');
// TypeScript knows getErrorMessage() returns Promise
const error = await loginPage.getErrorMessage();
expect(error).toBeTruthy();
});
`
If you call
loginPage.loginn() (typo), TypeScript catches it immediately. If you call loginPage.login(123, 'password') (wrong type), TypeScript errors.
Playwright's built-in types you'll use
`typescript
import { Page, BrowserContext, Locator, APIRequestContext } from '@playwright/test';
// Page — the main browser page object
// BrowserContext — an isolated browser session
// Locator — a reference to page elements
// APIRequestContext — for API testing
`
You import these when writing typed Page Objects or typed fixture files.
Typed test fixtures
When you extend Playwright's test fixtures (for sharing page objects across tests), TypeScript types make it reliable:
`typescript
import { test as base, Page } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
// Define the type of your custom fixtures
type Fixtures = {
loginPage: LoginPage;
};
// Extend the base test with typed fixtures
export const test = base.extend({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
});
// In tests, TypeScript knows loginPage is a LoginPage instance
test('test with fixture', async ({ loginPage }) => {
await loginPage.login('admin@becomeqa.com', 'testpass123');
});
`
tsconfig.json: what matters
When
npm init playwright@latest creates a tsconfig.json, it works out of the box. The settings worth knowing:
`json
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"strict": true, // ← This is the important one
"esModuleInterop": true,
"outDir": "./dist"
}
}
`
"strict": true enables all of TypeScript's strictness checks, including:
- No implicit
any type
Null checks (TypeScript warns when you might be calling methods on null)
The check that catches unawaited Promises
Keep
strict: true. The warnings it generates are almost always pointing to real problems.
What you don't need to learn (yet)
TypeScript has features that are useful for large-scale application development but rarely needed in test code:
- Generics: unless you're building reusable fixture utilities
- Decorators: used in older frameworks, not in modern Playwright
- Utility types (
Partial, Readonly): occasionally useful, not required
Declaration files ( .d.ts): for library authors, not test writers
Learn these when you encounter a specific problem they solve.
Common TypeScript errors in Playwright tests
Property 'X' does not exist on type 'Y'
You're calling a method or accessing a property that doesn't exist. Either a typo, or you need to import the right type.
Object is possibly 'null'
TypeScript knows a method might return
null. Check the return value:
`typescript
const text = await page.getByRole('heading').textContent();
// TypeScript: text might be null
if (text !== null) {
expect(text).toBe('My Travel Items');
}
// Or use the non-null assertion if you're certain:
expect(text!).toBe('My Travel Items');
`
Argument of type 'string' is not assignable to parameter of type 'number'
Wrong type passed to a function. Check the function signature and fix the argument.
'await' has no effect on the type of this expression
You're using
await on something that isn't a Promise. Remove the await or check why the method isn't returning a Promise.
JavaScript vs TypeScript: the practical decision
If you're starting from scratch: TypeScript. The learning curve over pure JavaScript is minimal, and the benefits (autocomplete, error catching) pay back immediately.
If you have an existing JavaScript Playwright project: consider migrating. Rename
.js to .ts, add a tsconfig.json, fix the type errors. For a typical project this takes a few hours.
If your team uses JavaScript: TypeScript files compile to JavaScript, so they coexist. You can migrate file by file.
FAQ
Do I need to compile TypeScript before running tests?
No. Playwright handles the compilation internally. You run
npx playwright test exactly as before. Playwright compiles TypeScript on the fly using its built-in transpiler.
TypeScript gives me errors, but the tests still run. Why?
TypeScript errors are warnings from the type checker, not runtime errors. The test runner compiles the code regardless of type errors. The point of fixing TypeScript errors is to catch bugs before they become runtime failures, not to make the tests run.
Do I need to understand all the TypeScript types Playwright uses?
No. You need to import
Page, Locator, and APIRequestContext` when writing typed page objects. The rest you'll encounter and look up as needed.
Is TypeScript significantly harder to learn than JavaScript for tests?
The meaningful additions for test code are: type annotations on function parameters, interfaces for test data, and importing Playwright types. If you already know JavaScript, learning these takes a few days, not weeks.
→ See also: Git & GitHub for QA Engineers: A No-Fluff Tutorial | Page Object Model in Playwright: From Messy to Maintainable