You already know JavaScript from the JavaScript for QA course, and you know TypeScript's type system from TypeScript for QA. This course is about the practical work of moving a real JavaScript test project over to TypeScript — the setup, the gotchas, and the payoff. This first lesson makes the case: here is exactly what TypeScript fixes in a QA codebase, with the kind of bugs you have probably already hit.
The bugs JavaScript silently allows
JavaScript's flexibility is the source of its most expensive QA bugs. Here are three that TypeScript catches at compile time — before your tests ever run.
Typos in property names. Rename a field in your test data factory and forget to update a caller. JavaScript returns undefined and keeps going.
// JS — this runs. The test fails three assertions later.
const user = createUser({ email: "alice@test.com", role: "admin" });
cy.get("[data-testid=username]").should("contain", user.eamil); // typo: eamil// TS — compile error before the test file loads
cy.get("[data-testid=username]").should("contain", user.eamil);
// Property 'eamil' does not exist on type 'User'. Did you mean 'email'?Wrong argument order. A login(email, password) helper called as login(password, email). Both are strings — JavaScript has no way to detect it.
// JS — both args are strings, so no error. Login fails with HTTP 401.
function login(email, password) { /* ... */ }
login("supersecret", "alice@test.com"); // flipped — silent bug// TS — even if both are strings, a named-object signature makes order irrelevant
function login({ email, password }: { email: string; password: string }) { /* ... */ }
login({ email: "alice@test.com", password: "supersecret" }); // correct
login({ password: "supersecret", email: "alice@test.com" }); // also correct — order doesn't matterCalling non-existent methods. A framework upgrades and renames .getText() to .innerText(). Every call site in JavaScript silently becomes undefined is not a function at runtime.
// JS — every caller breaks at runtime after the upgrade
const text = await element.getText(); // renamed to innerText() in v2// TS — compile error on every call site immediately after updating the type definition
const text = await element.getText();
// Property 'getText' does not exist on type 'Locator'. Did you mean 'innerText'?What this means for your test suite specifically
The TypeScript benefits that matter most in QA work — as distinct from general software engineering — cluster around four things.
Page object safety. When you define a page object with TypeScript, every method is known to the compiler. Misremember a method name and your editor shows a red squiggle before you run anything. Add // @ts-check to an existing JavaScript page object and you get a meaningful fraction of this without even renaming the file.
Test data shape validation. A typed TestUser interface means a missing required field (password, role, organisationId) is a compile error on the factory call — not a runtime failure halfway through a login flow.
interface TestUser {
email: string;
password: string;
role: "admin" | "member" | "guest";
}
function createUser(overrides: Partial<TestUser> = {}): TestUser {
return { email: "user@test.com", password: "password", role: "member", ...overrides };
}
createUser({ role: "superadmin" });
// Type '"superadmin"' is not assignable to type '"admin" | "member" | "guest"'API response types. In a typed API helper, response.data.user.eamil is a compile error. Without types, it returns undefined and the assertion fails with a message that points at the assertion, not the typo.
Playwright fixture types. Custom fixtures in Playwright get full intellisense when typed — every fixture property autocompletes in every test file that uses it.
Refactoring confidence
The operational win that compounds over time is safe refactoring. Rename a method on your LoginPage object:
- JavaScript: grep the project, update every usage you find, run the suite, discover two you missed.
- TypeScript: rename in the editor (F2 in VS Code), the compiler updates every reference in every file, run
npm run type-check, done.
This matters most when your test suite grows to hundreds of files. The cost of a missed rename grows with the size of the project; the cost with TypeScript stays flat.
The same codebase — JavaScript vs TypeScript
JavaScript test suite
Typos in property names surface at runtime
Wrong argument order is invisible
Renamed methods break on first run
IDE autocomplete guesses at object shapes
Rename a field → grep-and-pray across 200 files
TypeScript test suite
Property typos flagged as you type
Named-object signatures prevent wrong order
Renamed methods: compiler finds every call site
Autocomplete shows every method and field
Rename a field → compiler updates every reference
The IDE experience
VS Code with TypeScript gives you:
- Autocomplete on every page object method, fixture property, and response field — without reading the file that defines them.
- Inline documentation — hover over a function call and see its parameter types and return type without opening the definition.
- Jump to definition —
Cmd+Clickon any method takes you to where it's defined, even in a library's.d.tsfile. - Find all references — right-click a method and see every test file that calls it.
These are not luxuries. Over a large test suite, they are the difference between a morning spent tracking down a refactor and ten minutes of compiler-guided updates.
⚠️ Common mistakes
- Expecting zero bugs on day one. TypeScript catches type errors — logic errors are still your job. A test that navigates to the wrong page or asserts the wrong value passes the type checker just fine.
- Turning on strict mode then immediately disabling it when errors appear. Those errors are real problems. Disable strict temporarily during an incremental migration (lesson 3 of this chapter), but plan a path back to
strict: true. - Typing every variable manually. TypeScript's inference is excellent. Type function parameters and return values; let the compiler infer local variables from the assigned values.
🎯 Practice task
This is a recognition exercise — no TypeScript tooling required yet.
- Open the most complex JavaScript file in your current test project or any file from the JavaScript for QA course.
- Find three functions. For each, write a comment above it describing the expected type of every parameter and the return value. For example:
// email: string, password: string → void. - Find one place where a property is accessed on an object that comes from an external source (an API response, a fixture file, a helper's return value). Ask: if that property was renamed in the source, would JavaScript tell you? When would you find out?
- Find one place where a function is called with positional arguments. Ask: if two arguments of the same type were swapped, would the test fail immediately or silently?
You are identifying, by hand, the work TypeScript will do for you automatically. The next lesson covers when this work is not worth doing — and the lesson after that explains how to start the migration.