You met the same three primitives in JavaScript for QA — strings, numbers, booleans. TypeScript adds the type names you write in annotations and the strict checks that catch mismatches at compile time. This lesson is your reference for the three primitives you'll use constantly in test automation, plus how null, undefined, and void fit alongside them.
string — every URL, selector, and label
string is the type for any text. Single quotes, double quotes, and backticks all produce values of type string.
const testName: string = "Login with valid credentials";
const env: string = 'staging';
const baseUrl: string = "https://staging.myapp.com";
// Template literals work the same — type is still string
const apiUrl: string = `${baseUrl}/api/users`;Anywhere a Cypress or Playwright API expects a selector, URL, or label, the parameter is string:
// Cypress
cy.get(selector: string)
cy.visit(url: string)
// Playwright
page.goto(url: string)
page.locator(selector: string)number — counts, codes, and timeouts
number covers integers and decimals. There is no separate int or float — every numeric value is a number, just like in JavaScript.
const statusCode: number = 200;
const timeout: number = 5000;
const retryCount: number = 3;
const passRate: number = 87.5;You'll see this type on Cypress assertions and Playwright counts:
cy.get(".user-row").should("have.length", 5);
// ↑ number
await expect(page.getByRole("listitem")).toHaveCount(3);
// ↑ numberAPI response status codes and request durations are always number. If a string sneaks in (some APIs return "200" instead of 200), TypeScript catches the mismatch the moment it reaches a typed boundary.
boolean — yes/no decisions
boolean holds exactly true or false.
const isLoggedIn: boolean = true;
const shouldRetry: boolean = false;
const isProduction: boolean = process.env.NODE_ENV === "production";A naming convention worth keeping for test code: prefix booleans with is, has, should, or can. Reading the name aloud as a yes/no question is the test for whether you've named it well.
isVisible— yes/no, is it visible?hasError— yes/no, does it have an error?shouldSkip— yes/no, should this test skip?canRetry— yes/no, can the action be retried?
Cypress and Playwright assertions return — and accept — booleans constantly:
await expect(page.locator(".banner")).toBeVisible();
// response.ok ← boolean
// response.status ← number
// await response.text() ← stringnull and undefined under strict mode
When strict: true is set in your tsconfig.json (and you should always set it), the strictNullChecks flag is enabled. That flag changes how null and undefined work — they're no longer assignable to every type by default.
const middleName: string = null;
// ❌ Type 'null' is not assignable to type 'string'.To allow null, you spell it out as a union type:
const middleName: string | null = null; // ✅ explicit
const phone: string | undefined = undefined; // ✅ explicitThis is one of the highest-leverage changes strict mode makes. In JavaScript you'd assume a string was a string and crash later when it was actually null. In TypeScript, every place a value could be null is annotated, and you're forced to handle it before you read properties off it.
void — functions that don't return anything
A function that runs side effects and returns nothing has the return type void.
function logResult(message: string): void {
console.log(`[test] ${message}`);
}void and undefined are similar — a void-returning function technically returns undefined — but void signals intent: "callers should not rely on the return value." You'll see it on logging functions, setup hooks, and fire-and-forget side effects throughout test automation.
The five primitives at a glance
TypeScript primitives — what each one looks like in QA code
(Each bar is the same height — this is a labelled cheat-sheet of the primitives, not a measurement of anything.)
Putting them together
A realistic config for a Playwright suite, with every primitive in play:
const baseUrl: string = "https://staging.myapp.com";
const apiKey: string = "test-1234-abcd";
const defaultTimeout: number = 10_000;
const maxRetries: number = 3;
const isProduction: boolean = false;
const currentUser: string | null = null; // null = logged out
function logRunStart(env: string): void {
console.log(`Run starting against ${env}`);
}
logRunStart(baseUrl);Compare this to the JavaScript-for-QA equivalent and one thing stands out: the intent of every variable is now visible from the declaration line. A six-month-old codebase reads as if your past self left a comment on every value — because the type annotations are that comment.
⚠️ Common mistakes
- Treating
nullandundefinedas interchangeable. WithstrictNullCheckson,string | nullandstring | undefinedare different types. JavaScript habit says they're "the same kind of nothing"; TypeScript treats them separately. Pick one in your codebase (most teams usenullfor "intentionally empty" andundefinedfor "not yet set") and stick to it. - Annotating obvious primitives.
const count: number = 5annotates what TypeScript can already see. Inference handles literal initializers fine — annotate function parameters and tricky cases, not everyconst. - Comparing strings and numbers without checking. TypeScript is strict about
===between unrelated types."200" === 200is a compile error. That's the language refusing to let a real test bug through — fix the source so both sides agree on the type rather than reaching for==to coerce.
🎯 Practice task
Build a typed test config. 15-20 minutes.
- In your
ts-for-qa/srcfolder, createconfig.ts. - Declare these annotated constants:
baseUrl: stringapiKey: string(any fake value)defaultTimeout: numbermaxRetries: numberisProduction: booleancurrentUser: string | null(set tonullfor now)
- Write a function
logConfig(): voidthat prints each value with its name and type usingtypeof. - Run with
npx ts-node src/config.ts. Confirm every value prints with the right type label. - Now break it deliberately. Try each of the following, run the compiler, read the error, then revert:
const baseUrl: string = 1234;const isProduction: boolean = "false";const currentUser: string = null;(without the| null)
- Stretch: add a function
formatStatus(code: number): stringthat returns"OK"for 200/201 and"ERROR"otherwise, and call it on a few values. Notice how the parameter and return type lock the contract.
You've now seen every primitive type you'll use day to day. The next lesson scales up from single values to collections — arrays and tuples.