Module 3: TypeScript & JavaScript for Playwright

Modern QA

Module 3: TypeScript & JavaScript for Playwright

Playwright tests are programs, and the language they are written in is TypeScript—a typed layer on top of JavaScript. You do not need to be a full-stack developer to write excellent tests, but you do need a working command of a focused slice of the language: variables and types, functions and arrow syntax, the async/await model that underpins every browser action, and how modules import and export code. This module teaches exactly that slice, with every example aimed at testing. Get comfortable here and the rest of the course reads like plain English.

What you’ll learn

By the end of this lesson you will be able to:

  • Declare variables with const and let and give them types.
  • Use the core types testers reach for: string, number, boolean, arrays, and objects.
  • Write functions, including concise arrow functions.
  • Explain promises and use async/await correctly—the most important idea for Playwright.
  • Import and export code with ES modules.
  • Say why TypeScript’s types make Playwright work safer and faster to write.

JavaScript and TypeScript, briefly

JavaScript (JS) is the language browsers run. Node.js lets it run outside the browser too, which is how Playwright executes your tests. TypeScript (TS) is JavaScript with a type system bolted on: you can annotate what kind of value a variable holds, and a compiler checks your code before it runs, catching whole classes of mistakes. TypeScript compiles down to ordinary JavaScript, so anything you learn about JS applies directly—TS just adds safety on top.

You chose TypeScript when initializing the project in Module 2, and this module shows why that was the right call.

Variables and types

Declare variables with const (a value that will not be reassigned) or let (one that will). Prefer const by default and only reach for let when you genuinely need to reassign. Avoid the old var entirely.

const baseUrl = 'https://example.com';   // will not change
let attempts = 0;                        // will be reassigned
attempts = attempts + 1;

// baseUrl = 'other';  // error: cannot reassign a const

TypeScript can often infer the type from the value, but you can also state it explicitly after a colon:

const username: string = 'qa_user';
const retryCount: number = 3;
const isHeadless: boolean = true;

// Arrays
const browsers: string[] = ['chromium', 'firefox', 'webkit'];

// Objects with a described shape
const credentials: { user: string; pass: string } = {
  user: 'qa_user',
  pass: 'secret123',
};

Those type annotations are the whole point of TypeScript. If you later write const retryCount: number = 'three', the compiler stops you before the test ever runs.

Tutor tip: let inference do the work

You rarely need to annotate everything. const retryCount = 3 is already typed as number by inference. Reserve explicit annotations for function parameters, return types, and places where the type is not obvious. Clean test code leans on inference and annotates only where it adds clarity.

Functions and arrow functions

A function packages reusable logic. The traditional form uses the function keyword:

function buildUrl(path: string): string {
  return 'https://example.com' + path;
}

const loginUrl = buildUrl('/login');   // 'https://example.com/login'

Note the parameter type (path: string) and the return type (: string) after the parentheses. TypeScript will warn you if you return the wrong kind of value or pass the wrong kind of argument.

Modern JS favours the shorter arrow function, which you have already seen in every Playwright test:

// Same function as an arrow function
const buildUrl = (path: string): string => {
  return 'https://example.com' + path;
};

// For a one-line body you can drop the braces and 'return'
const double = (n: number): number => n * 2;

This is exactly the shape of a Playwright test body: test('name', async ({ page }) => { ... }). The async ({ page }) => { ... } part is an arrow function—now you can read it precisely.

Promises and async/await

This is the most important section in the module. Browser actions are asynchronous: clicking a button, loading a page, and reading text all take time, and JavaScript does not stop the world to wait. Instead these operations return a promise—an object representing a result that will arrive later.

If you ignore the promise, you move on before the action finishes, and your test checks a page that has not updated yet. The await keyword solves this: it pauses the function until the promise resolves, then hands you the result. await is only allowed inside a function marked async—which is why every Playwright test body is async.

test('reads the welcome message', async ({ page }) => {
  await page.goto('https://example.com/login');   // wait for navigation
  await page.getByLabel('Username').fill('qa_user'); // wait for the fill
  await page.getByRole('button', { name: 'Log in' }).click(); // wait for click

  // Now read text. textContent returns a promise, so we await it.
  const heading = await page.getByRole('heading').textContent();
  expect(heading).toContain('Welcome');
});

The rule of thumb for Playwright: if a call talks to the browser, await it. Navigation, actions, and locator queries like textContent() all return promises. The web-first expect(locator)... assertions are also awaited. The plain-value expect(heading) on a string you already have is not.

Pitfall: the forgotten await

The number-one beginner bug in Playwright is a missing await. Consider:

// WRONG: no await
test('bad', async ({ page }) => {
  page.goto('https://example.com');   // promise ignored!
  expect(page.getByText('Welcome')).toBeVisible(); // runs too early
});

Without await, goto fires but the test rushes ahead before the page loads, producing confusing, flaky failures—or a warning about an unhandled promise. The fix is to await every browser call. Many teams add an ESLint rule (no-floating-promises) that flags any un-awaited promise automatically, turning this runtime bug into a compile-time warning.

Modules: import and export

Real test suites span many files: helpers in one file, page objects in another, test data in a third. ES modules let files share code. A file exports what it wants to share; another file imports it.

// file: helpers/urls.ts
export const BASE_URL = 'https://example.com';

export function loginUrl(): string {
  return BASE_URL + '/login';
}
// file: tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { BASE_URL, loginUrl } from '../helpers/urls';

test('navigates to login', async ({ page }) => {
  await page.goto(loginUrl());
  await expect(page).toHaveURL(BASE_URL + '/login');
});

You have already used the named import form every time you wrote import { test, expect } from '@playwright/test'. A module can also have a default export—you will see export default defineConfig({ ... }) in playwright.config.ts in Module 5.

Why TypeScript helps in Playwright

You could write Playwright tests in plain JavaScript, but TypeScript pays for itself immediately in a test codebase.

  • Autocomplete. Because Playwright ships rich type definitions, your editor knows every method on page and every option it accepts. Type page. and a complete, accurate list appears. You spend far less time hunting through documentation.
  • Catching errors before they run. Misspell a method or pass the wrong option type and TypeScript flags it in the editor, not three minutes into a CI run. For a suite that takes time to execute, catching mistakes at write-time is a large saving.
  • Safer refactoring. Rename a helper or change a page object’s method and the compiler shows you every call site that needs updating. This is what makes large suites maintainable.
  • Self-documenting code. Types describe intent. A function typed (user: Credentials) => Promise<void> tells the next reader exactly what it expects and returns.

From the field: types turn runtime bugs into editor squiggles

On a JavaScript suite, a typo like page.getByRol(...) blows up only when that test runs—possibly deep in a CI pipeline, minutes later. On a TypeScript suite the same typo is a red underline the instant you type it. Multiply that across hundreds of tests and dozens of helpers and the difference is enormous: TypeScript shifts a whole category of defects left, from “discovered in CI” to “impossible to commit”. That is the shift-left principle from Module 1 applied to your own test code.

Hands-on exercise

In your project from Module 2:

  1. Create helpers/data.ts exporting a const BASE_URL and a function fullUrl(path: string): string that joins them. Import both into a spec and use them.
  2. Write an arrow function isValidEmail = (email: string): boolean that returns whether a string contains '@'. Call it and log the result.
  3. Take a working test and deliberately remove one await. Run it and observe the flaky or unhandled-promise behaviour, then restore the await.
  4. In your editor, type page. inside a test and browse the autocomplete list. Find three methods you have not used yet and read their descriptions.
  5. Declare const retries: number = 'three' on purpose and watch TypeScript reject it before you ever run the test.

Knowledge check

When should you use const versus let?

Use const by default for any value you will not reassign; use let only when you genuinely need to reassign the variable. Avoid var. Defaulting to const makes intent clear and prevents accidental reassignment.

What is a promise, and what does await do?

A promise represents a value that will be available later—the result of an asynchronous operation like a navigation or click. await pauses the surrounding async function until the promise resolves, then returns its result, so your code runs in the right order.

Why is every Playwright test body marked async?

Because browser actions are asynchronous and must be awaited, and await is only allowed inside an async function. Marking the test body async lets you await each browser call.

What goes wrong if you forget an await on page.goto?

The navigation starts but the test continues immediately, checking the page before it has loaded. This causes confusing, flaky failures or an unhandled-promise warning. The fix is to await every browser call; a lint rule can catch the omission automatically.

Give two concrete ways TypeScript improves Playwright authoring.

Any two of: accurate autocomplete for the Playwright API; catching typos and wrong option types in the editor instead of at runtime; safe refactoring because the compiler flags every affected call site; and self-documenting code through types.

Key takeaways

  • TypeScript is JavaScript plus a compile-time type checker; it compiles to plain JS and adds safety.
  • Prefer const, annotate where it adds clarity, and let inference handle the rest.
  • Arrow functions are the shape of every Playwright test body.
  • Browser actions return promises; await them inside async functions—forgetting await is the classic bug.
  • Modules share code via import/export; TypeScript’s types give you autocomplete, early error detection, and safe refactoring.

Go deeper

A few language features that make test code cleaner.

Destructuring. The { page } in a test signature is object destructuring—pulling a named property out of an object. It works anywhere: const { user, pass } = credentials; grabs both fields in one line. Playwright leans on this heavily for fixtures.

  • Template literals. Backtick strings let you embed values: `${BASE_URL}/users/${id}` is cleaner than string concatenation and is the idiomatic way to build URLs and messages.
  • Optional chaining and nullish coalescing. user?.name safely reads a property that might be undefined, and value ?? 'default' supplies a fallback only when the left side is null or undefined. Both prevent a common class of crashes when data is missing.
  • Interfaces and types for test data. Defining interface Credentials { user: string; pass: string } once lets you type every function that handles logins, so the compiler guards your whole data flow—a foundation for the structured test data you will build in Module 5.

Save to list

Scroll to Top