TypeScript Data Types Practice Programs with Solutions (31 Programs)

Practise TypeScript data types with 31 programs, each with a full solution, the exact output and a short explanation. The programs start with the basics, move to intermediate problems and finish with real-world scenarios from test automation, such as test data, prices, form inputs and test results. They are written for testers learning TypeScript for Playwright.

How to practise: read the task, write your own solution first, then open Show solution to compare. Run any program with npx tsx program.ts (or compile it with npx tsc).

New to this topic? Read the TypeScript Data Types lesson first.

TypeScript Beginner Programs

1. The primitive typesBeginner

Declare one variable of each primitive type and print its runtime type.

Show solution
const title: string = 'Login test';
const duration: number = 2.5;
const passed: boolean = true;
const bigId: bigint = 9007199254740993n;
const key: symbol = Symbol('id');
console.log(typeof title, typeof duration, typeof passed, typeof bigId, typeof key);

Output

string number boolean bigint symbol

Explanation: TypeScript’s primitive types match JavaScript’s runtime types.

2. null and undefinedBeginner

Declare variables that can be null or undefined and print them.

Show solution
let selectedOption: string | null = null;
let middleName: string | undefined;
console.log(selectedOption, middleName);
selectedOption = 'Express';
console.log(selectedOption);

Output

null undefined
Express

Explanation: with strictNullChecks, null and undefined must be included in the type explicitly.

3. Arrays in two notationsBeginner

Declare number arrays with number[] and Array<number>.

Show solution
const scores: number[] = [90, 75, 88];
const timings: Array<number> = [120, 340];
console.log(scores.length + timings.length);
console.log(Math.max(...scores));

Output

5
90

Explanation: number[] and Array&lt;number&gt; are the same type. The shorter form is more common.

4. TupleBeginner

Store a username and password pair in a tuple.

Show solution
const credentials: [string, string] = ['qa_user', 'Secret123'];
const [user, pass] = credentials;
console.log(user, pass.length);

Output

qa_user 9

Explanation: a tuple has a fixed length and a type for each position.

5. Tuple with different typesBeginner

Store an HTTP status code and message as [number, string].

Show solution
const response: [number, string] = [404, 'Not Found'];
console.log(`${response[0]} ${response[1]}`);
// @ts-expect-error: Type 'string' is not assignable to type 'number'
const wrong: [number, string] = ['404', 'Not Found'];
console.log(wrong.length);

Output

404 Not Found
2

Explanation: each tuple position is checked separately, so swapped or wrong types are compile errors.

6. Numeric enumBeginner

Create a Priority enum and print a value and its name.

Show solution
enum Priority { Low, Medium, High }
const p: Priority = Priority.High;
console.log(p);
console.log(Priority[p]);

Output

2
High

Explanation: numeric enums start at 0. Reverse mapping turns a number back into its name.

7. String enumBeginner

Create a Browser string enum and use it in a message.

Show solution
enum Browser {
  Chromium = 'chromium',
  Firefox = 'firefox',
  WebKit = 'webkit'
}
const target: Browser = Browser.Firefox;
console.log(`Running on ${target}`);

Output

Running on firefox

Explanation: string enum members have readable values, which is useful in logs and reports.

8. void return typeBeginner

Write a logging function with a void return type.

Show solution
function logStep(step: string): void {
  console.log(`STEP: ${step}`);
}
const result = logStep('Open login page');
console.log(result);

Output

STEP: Open login page
undefined

Explanation: void means the function returns nothing useful. At runtime it returns undefined.

9. Object typeBeginner

Describe a product with an inline object type.

Show solution
const product: { sku: string; price: number; tags: string[] } = {
  sku: 'KB-01',
  price: 1499,
  tags: ['electronics', 'keyboard']
};
console.log(product.sku, product.price, product.tags.join('/'));

Output

KB-01 1499 electronics/keyboard

Explanation: object types list each property name with its type.

10. Date typeBeginner

Create a Date from a fixed ISO string and print parts of it in UTC.

Show solution
const runDate: Date = new Date('2026-11-02T09:30:00Z');
console.log(runDate.toISOString());
console.log(runDate.getUTCFullYear(), runDate.getUTCMonth() + 1, runDate.getUTCDate());

Output

2026-11-02T09:30:00.000Z
2026 11 2

Explanation: built-in classes such as Date are types too. UTC methods avoid time-zone differences.

TypeScript Intermediate Programs

11. unknown vs anyIntermediate

Assign the same value to any and unknown and see which requires a check.

Show solution
const fromApi: unknown = JSON.parse('{"id": 7}');
const anything: any = JSON.parse('{"id": 7}');
console.log(anything.id);
if (typeof fromApi === 'object' && fromApi !== null && 'id' in fromApi) {
  console.log((fromApi as { id: number }).id);
}

Output

7
7

Explanation: unknown forces you to check the shape first, while any lets you access anything without checks.

12. never for exhaustive checksIntermediate

Use never to make sure every status in a union is handled.

Show solution
type Status = 'passed' | 'failed' | 'skipped';
function label(status: Status): string {
  switch (status) {
    case 'passed': return 'βœ”';
    case 'failed': return '✘';
    case 'skipped': return '-';
    default: {
      const unreachable: never = status;
      return unreachable;
    }
  }
}
console.log(['passed', 'failed', 'skipped'].map(s => label(s as Status)).join(' '));

Output

βœ” ✘ -

Explanation: if a new status is added to the union, the never assignment fails to compile until it is handled.

13. Union of object typesIntermediate

Model an API result that is either success or error, and print it.

Show solution
type ApiResult = { ok: true; data: string[] } | { ok: false; error: string };
const results: ApiResult[] = [{ ok: true, data: ['a', 'b'] }, { ok: false, error: 'Timeout' }];
for (const r of results) {
  console.log(r.ok ? `Got ${r.data.length} items` : `Failed: ${r.error}`);
}

Output

Got 2 items
Failed: Timeout

Explanation: checking the ok property narrows the union, so data and error are only accessible in the right branch.

14. Intersection typesIntermediate

Combine a User type and a Timestamps type into one.

Show solution
type User = { id: number; name: string };
type Timestamps = { createdAt: string };
type UserRecord = User & Timestamps;
const rec: UserRecord = { id: 1, name: 'Asha', createdAt: '2026-11-01' };
console.log(rec.name, rec.createdAt);

Output

Asha 2026-11-01

Explanation: A &amp; B requires all properties of both types.

15. Type assertion with asIntermediate

Parse JSON and tell TypeScript its shape with as.

Show solution
type Config = { retries: number; baseURL: string };
const config = JSON.parse('{"retries":2,"baseURL":"https://qa.app.test"}') as Config;
console.log(config.retries + 1, config.baseURL);

Output

3 https://qa.app.test

Explanation: as tells the compiler what type to assume. It does not check the data at runtime, so use it only when you trust the source.

16. Optional tuple elementsIntermediate

Use a tuple with an optional third element for a test step.

Show solution
type Step = [action: string, target: string, value?: string];
const steps: Step[] = [['click', 'Login'], ['fill', 'Email', 'qa@test.com']];
for (const [action, target, value] of steps) {
  console.log(value ? `${action} ${target} = ${value}` : `${action} ${target}`);
}

Output

click Login
fill Email = qa@test.com

Explanation: labelled tuple elements document each position, and ? makes the last one optional.

17. Readonly tupleIntermediate

Create a read-only coordinate pair and show the compile error when changing it.

Show solution
const viewport: readonly [number, number] = [1280, 720];
console.log(`${viewport[0]}x${viewport[1]}`);
// @ts-expect-error: Cannot assign to '0' because it is a read-only property
viewport[0] = 1920;

Output

1280x720

Explanation: readonly tuples protect fixed values such as default viewport sizes.

18. Index signaturesIntermediate

Type an object of HTTP headers with unknown keys.

Show solution
const headers: { [name: string]: string } = { 'content-type': 'application/json' };
headers['x-request-id'] = 'abc-123';
console.log(Object.keys(headers).length, headers['content-type']);

Output

2 application/json

Explanation: an index signature allows any string key with values of the declared type.

19. Function typesIntermediate

Declare a variable whose type is a function from string to boolean.

Show solution
type Validator = (value: string) => boolean;
const isEmail: Validator = value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
const isNotEmpty: Validator = value => value.trim().length > 0;
console.log(isEmail('qa@test.com'), isEmail('qa'), isNotEmpty('  '));

Output

true false false

Explanation: function types describe parameters and return types, so any function assigned must match.

20. bigint arithmeticIntermediate

Add two large order IDs as bigint values.

Show solution
const base: bigint = 9007199254740991n;
const next: bigint = base + 2n;
console.log(next.toString());
try {
  // @ts-expect-error: Operator '+' cannot be applied to types 'bigint' and 'number'
  console.log(base + 1);
} catch (error) {
  console.log('Also fails at runtime:', error instanceof TypeError);
}

Output

9007199254740993
Also fails at runtime: true

Explanation: TypeScript rejects mixing bigint and number at compile time. The same mistake would throw a TypeError in plain JavaScript, so the compiler saves you from a runtime crash.

TypeScript Real-World QA Scenario Programs

21. Typed API responseReal-world

Describe a paginated API response and read its fields.

Show solution
type Product = { id: number; name: string; price: number };
type Page<T> = { items: T[]; page: number; total: number };
const response: Page<Product> = {
  items: [{ id: 1, name: 'Mouse', price: 599 }, { id: 2, name: 'Cable', price: 199 }],
  page: 1,
  total: 12
};
console.log(`Page ${response.page}: ${response.items.length} of ${response.total} items`);
console.log(response.items.map(p => p.name).join(', '));

Output

Page 1: 2 of 12 items
Mouse, Cable

Explanation: a typed response shape gives autocomplete and catches typos such as respose.itmes.

22. Status enum in test resultsReal-world

Use a string enum for test statuses and count them.

Show solution
enum TestStatus { Passed = 'passed', Failed = 'failed', Skipped = 'skipped' }
const runs: TestStatus[] = [TestStatus.Passed, TestStatus.Failed, TestStatus.Passed];
const failed = runs.filter(s => s === TestStatus.Failed).length;
console.log(`${runs.length} runs, ${failed} failed`);

Output

3 runs, 1 failed

Explanation: an enum gives one source of truth for allowed status values.

23. Tuple for table rowsReal-world

Type web-table rows as tuples and total the price column.

Show solution
type Row = [name: string, qty: number, price: number];
const rows: Row[] = [['Pen', 3, 20], ['Book', 1, 350], ['Bag', 2, 899]];
const total = rows.reduce((sum, [, qty, price]) => sum + qty * price, 0);
console.log('Order total:', total);

Output

Order total: 2208

Explanation: tuples fit fixed-column data such as table rows, and destructuring reads the columns by position.

24. Discriminated union for UI eventsReal-world

Handle click and fill events with a discriminated union.

Show solution
type Action =
  | { type: 'click'; selector: string }
  | { type: 'fill'; selector: string; value: string }
  | { type: 'goto'; url: string };
const actions: Action[] = [
  { type: 'goto', url: '/login' },
  { type: 'fill', selector: '#email', value: 'qa@test.com' },
  { type: 'click', selector: 'button[type=submit]' }
];
for (const a of actions) {
  if (a.type === 'goto') console.log('Navigate to', a.url);
  else if (a.type === 'fill') console.log(`Fill ${a.selector} with ${a.value}`);
  else console.log('Click', a.selector);
}

Output

Navigate to /login
Fill #email with qa@test.com
Click button[type=submit]

Explanation: the shared type field tells TypeScript which properties each action has.

25. Validate unknown JSONReal-world

Write a type guard that checks unknown JSON before using it as a User.

Show solution
type User = { id: number; email: string };
function isUser(value: unknown): value is User {
  return typeof value === 'object' && value !== null &&
    typeof (value as User).id === 'number' && typeof (value as User).email === 'string';
}
for (const text of ['{"id":1,"email":"qa@test.com"}', '{"id":"1"}']) {
  const data: unknown = JSON.parse(text);
  console.log(isUser(data) ? `Valid user ${data.email}` : 'Invalid user data');
}

Output

Valid user qa@test.com
Invalid user data

Explanation: a type guard (value is User) checks at runtime and narrows the type at compile time.

26. Nullable fields from a formReal-world

Handle a form object where optional fields may be null.

Show solution
type Form = { name: string; company: string | null; phone?: string };
const forms: Form[] = [
  { name: 'Asha', company: 'Acme', phone: '98765' },
  { name: 'Ravi', company: null }
];
for (const f of forms) {
  console.log(`${f.name} | ${f.company ?? 'Individual'} | ${f.phone ?? '-'}`);
}

Output

Asha | Acme | 98765
Ravi | Individual | -

Explanation: null (an explicit empty value) and optional fields are both handled with ??.

27. Record of expected textsReal-world

Store expected page titles in a Record keyed by route.

Show solution
type Route = '/' | '/login' | '/cart';
const expectedTitles: Record<Route, string> = { '/': 'Home', '/login': 'Sign in', '/cart': 'Your cart' };
const visited: Route[] = ['/login', '/cart'];
for (const route of visited) console.log(`${route} -> ${expectedTitles[route]}`);

Output

/login -> Sign in
/cart -> Your cart

Explanation: Record&lt;Route, string&gt; requires an entry for every route, so none can be forgotten.

28. Symbol as a unique keyReal-world

Use a symbol to attach hidden metadata to a test object.

Show solution
const meta: unique symbol = Symbol('meta');
const test = { title: 'Login', [meta]: { retries: 2 } };
console.log(Object.keys(test));
console.log(test[meta].retries);

Output

[ 'title' ]
2

Explanation: symbol keys do not appear in Object.keys, so metadata does not interfere with normal properties.

29. Date arithmetic with typesReal-world

Calculate a token expiry 30 minutes after a fixed issue time.

Show solution
const issuedAt: Date = new Date('2026-11-02T10:00:00Z');
const expiresAt: Date = new Date(issuedAt.getTime() + 30 * 60 * 1000);
const remainingMin: number = (expiresAt.getTime() - issuedAt.getTime()) / 60000;
console.log(expiresAt.toISOString(), remainingMin);

Output

2026-11-02T10:30:00.000Z 30

Explanation: getTime() returns a number of milliseconds, which makes date arithmetic type-safe.

30. Map with typed keys and valuesReal-world

Use a Map&lt;string, number&gt; to track response times per endpoint.

Show solution
const timings = new Map<string, number>();
timings.set('/api/login', 220);
timings.set('/api/cart', 480);
timings.set('/api/login', 180);
console.log(timings.size, timings.get('/api/login'));
const slow = [...timings].filter(([, ms]) => ms > 300).map(([url]) => url);
console.log('Slow:', slow.join(', '));

Output

2 180
Slow: /api/cart

Explanation: Map&lt;K, V&gt; types both keys and values. Setting an existing key replaces its value.

31. Set of unique tagsReal-world

Collect unique tags from several tests into a Set&lt;string&gt;.

Show solution
const testTags: string[][] = [['smoke', 'login'], ['regression', 'login'], ['smoke', 'cart']];
const unique = new Set<string>();
for (const tags of testTags) tags.forEach(t => unique.add(t));
console.log(unique.size, [...unique].sort().join(', '));

Output

4 cart, login, regression, smoke

Explanation: a typed Set stores each value once and only accepts the declared type.

Keep Practising

TypeScript Variables Practice Programs with Solutions (31 Programs)

Practise TypeScript variables and types with 31 programs, each with a full solution, the exact output and a short explanation. The programs start with the basics, move to intermediate problems and finish with real-world scenarios from test automation, such as test data, prices, form inputs and test results. They are written for testers learning TypeScript for Playwright.

How to practise: read the task, write your own solution first, then open Show solution to compare. Run any program with npx tsx program.ts (or compile it with npx tsc).

New to this topic? Read the TypeScript Variables lesson first.

TypeScript Beginner Programs

1. Annotate basic variablesBeginner

Declare a string, a number and a boolean with explicit type annotations and print them.

Show solution
const testerName: string = 'Asha';
const experienceYears: number = 4;
const isLead: boolean = false;
console.log(testerName, experienceYears, isLead);

Output

Asha 4 false

Explanation: name: type tells TypeScript what a variable may hold, and the compiler checks every assignment against it.

2. Type inferenceBeginner

Declare variables without annotations and print their runtime types.

Show solution
let browser = 'chromium';
let retries = 2;
let headless = true;
console.log(typeof browser, typeof retries, typeof headless);

Output

string number boolean

Explanation: TypeScript infers string, number and boolean from the initial values, so annotations are often unnecessary.

3. Catch a wrong assignment at compile timeBeginner

Show that assigning a string to a number variable is a compile error, then print the runtime type.

Show solution
let count: number = 5;
// @ts-expect-error: Type 'string' is not assignable to type 'number'
count = 'five';
console.log(typeof count);

Output

string

Explanation: the compiler rejects the assignment. @ts-expect-error documents the expected error. Types are erased at runtime, which is why JavaScript still runs the line.

4. const narrows to a literal typeBeginner

Compare the inferred types of a const and a let holding 'qa'.

Show solution
const envConst = 'qa';
let envLet = 'qa';
envLet = 'staging';
const check: 'qa' = envConst;
console.log(check, envLet);

Output

qa staging

Explanation: a const string is inferred as the literal type 'qa', while a let widens to string because it can change.

5. Union type variableBeginner

Create a variable that may hold a number or a string ID.

Show solution
let userId: number | string = 101;
console.log(userId);
userId = 'USR-101';
console.log(userId);

Output

101
USR-101

Explanation: a union type A | B allows any of the listed types.

6. Literal union for allowed valuesBeginner

Restrict a browser variable to three allowed names.

Show solution
type BrowserName = 'chromium' | 'firefox' | 'webkit';
let browser: BrowserName = 'firefox';
console.log(browser);
// @ts-expect-error: '"edge"' is not assignable to type 'BrowserName'
browser = 'edge';

Output

firefox

Explanation: literal unions turn typos and unsupported values into compile errors.

7. Type aliasBeginner

Create a Url type alias and use it for two variables.

Show solution
type Url = string;
const baseUrl: Url = 'https://qa.shop.test';
const loginUrl: Url = `${baseUrl}/login`;
console.log(loginUrl);

Output

https://qa.shop.test/login

Explanation: a type alias gives a meaningful name to a type, which documents intent.

8. Typed array variableBeginner

Declare an array of test names with a type annotation and add an item.

Show solution
const tests: string[] = ['login', 'search'];
tests.push('checkout');
console.log(tests);
console.log(tests.length);

Output

[ 'login', 'search', 'checkout' ]
3

Explanation: string[] means “array of strings”, so only strings can be pushed.

9. Typed object variableBeginner

Declare a user variable with an inline object type.

Show solution
const user: { name: string; age: number; active: boolean } = { name: 'Ravi', age: 29, active: true };
console.log(`${user.name} (${user.age})`);

Output

Ravi (29)

Explanation: inline object types describe every property and its type.

10. Optional propertyBeginner

Declare an object type where phone is optional and create two users.

Show solution
type Contact = { email: string; phone?: string };
const a: Contact = { email: 'a@test.com' };
const b: Contact = { email: 'b@test.com', phone: '9876543210' };
console.log(a.phone ?? 'no phone', '|', b.phone ?? 'no phone');

Output

no phone | 9876543210

Explanation: ? marks a property as optional, so its type becomes string | undefined.

TypeScript Intermediate Programs

11. readonly propertiesIntermediate

Make id read-only and show the compile error when changing it.

Show solution
type TestCase = { readonly id: string; title: string };
const tc: TestCase = { id: 'TC-1', title: 'Login' };
tc.title = 'Login with SSO';
// @ts-expect-error: Cannot assign to 'id' because it is a read-only property
tc.id = 'TC-2';
console.log(tc.title);

Output

Login with SSO

Explanation: readonly prevents reassignment at compile time.

12. readonly arraysIntermediate

Declare a read-only list of supported browsers.

Show solution
const SUPPORTED: readonly string[] = ['chromium', 'firefox', 'webkit'];
console.log(SUPPORTED.includes('webkit'));
// @ts-expect-error: Property 'push' does not exist on type 'readonly string[]'
SUPPORTED.push('edge');

Output

true

Explanation: read-only arrays have no mutating methods in their type, which protects shared constants.

13. as constIntermediate

Freeze a config object’s types to literals with as const.

Show solution
const config = { browser: 'chromium', workers: 4 } as const;
const browser: 'chromium' = config.browser;
console.log(browser, config.workers);

Output

chromium 4

Explanation: as const makes properties readonly and keeps literal types instead of widening them to string or number.

14. Derive a union from an as const arrayIntermediate

Create a Env type from an array of environment names.

Show solution
const ENVS = ['dev', 'qa', 'staging'] as const;
type Env = (typeof ENVS)[number];
const current: Env = 'qa';
console.log(current, ENVS.length);

Output

qa 3

Explanation: (typeof ENVS)[number] turns the array’s literal values into a union type, so the list and the type stay in sync.

15. unknown requires narrowingIntermediate

Store a value of type unknown and narrow it before using it.

Show solution
let input: unknown = 'qa@example.com';
if (typeof input === 'string') {
  console.log(input.toUpperCase());
}
input = 42;
if (typeof input === 'number') {
  console.log(input.toFixed(1));
}

Output

QA@EXAMPLE.COM
42.0

Explanation: unknown accepts anything, but you must check its type before using it, unlike any.

16. any disables checkingIntermediate

Show that any allows unsafe operations that fail at runtime.

Show solution
let data: any = 'text';
try {
  data.notAMethod();
} catch (error) {
  console.log(error instanceof TypeError ? 'Runtime TypeError' : 'Other error');
}

Output

Runtime TypeError

Explanation: any turns off type checking, so mistakes surface only at runtime. Prefer unknown.

17. Definite assignmentIntermediate

Declare a variable that is assigned inside a branch and use it safely.

Show solution
let status: string;
const passed = 42 > 40;
if (passed) {
  status = 'PASSED';
} else {
  status = 'FAILED';
}
console.log(status);

Output

PASSED

Explanation: TypeScript tracks that status is assigned on every path, so reading it afterwards is allowed.

18. Template literal typesIntermediate

Create a TestId type that must look like TC-&lt;number&gt;.

Show solution
type TestId = `TC-${number}`;
const first: TestId = 'TC-101';
// @ts-expect-error: '"101"' is not assignable to type '`TC-${number}`'
const bad: TestId = '101';
console.log(first, bad);

Output

TC-101 101

Explanation: template literal types describe string patterns, which catches malformed IDs.

19. Destructuring with typesIntermediate

Destructure a typed credentials object into variables.

Show solution
type Credentials = { username: string; password: string; role?: 'admin' | 'viewer' };
const creds: Credentials = { username: 'qa_user', password: 'Secret123' };
const { username, role = 'viewer' } = creds;
console.log(username, role);

Output

qa_user viewer

Explanation: destructured variables keep their property types. The default value narrows role to the literal union.

20. Block-scoped typed variablesIntermediate

Use the same variable name with different types in two blocks.

Show solution
{
  const result: number = 95;
  console.log('score', result);
}
{
  const result: string = 'passed';
  console.log('status', result);
}

Output

score 95
status passed

Explanation: const and let are block-scoped, so each block can declare its own result with its own type.

TypeScript Real-World QA Scenario Programs

21. Environment URL mapReal-world

Use a Record to store base URLs for each environment and look one up.

Show solution
type Env = 'qa' | 'staging' | 'prod';
const BASE_URLS: Record<Env, string> = {
  qa: 'https://qa.shop.test',
  staging: 'https://staging.shop.test',
  prod: 'https://shop.test'
};
const env: Env = 'staging';
console.log(`${BASE_URLS[env]}/login`);

Output

https://staging.shop.test/login

Explanation: Record&lt;Env, string&gt; forces a URL for every environment, so a missing one is a compile error.

22. Typed test configurationReal-world

Declare a typed config object with optional settings and defaults.

Show solution
type RunConfig = { baseURL: string; retries?: number; headless?: boolean };
const config: RunConfig = { baseURL: 'https://qa.app.test' };
const retries = config.retries ?? 0;
const headless = config.headless ?? true;
console.log(config.baseURL, retries, headless);

Output

https://qa.app.test 0 true

Explanation: optional properties plus ?? defaults is a typical shape for test settings.

23. Typed counters for resultsReal-world

Count test results with a typed counter object.

Show solution
type Status = 'passed' | 'failed' | 'skipped';
const counts: Record<Status, number> = { passed: 0, failed: 0, skipped: 0 };
const results: Status[] = ['passed', 'failed', 'passed', 'skipped', 'passed'];
for (const r of results) counts[r]++;
console.log(counts);

Output

{ passed: 3, failed: 1, skipped: 1 }

Explanation: indexing a Record&lt;Status, number&gt; with a Status value is fully type-safe.

24. Typed test userReal-world

Create a typed test user and build an email from it.

Show solution
type Role = 'admin' | 'editor' | 'viewer';
type TestUser = { id: number; name: string; role: Role };
const user: TestUser = { id: 7, name: 'meera', role: 'editor' };
const email = `${user.name}.${user.id}@example.com`;
console.log(email, user.role);

Output

meera.7@example.com editor

Explanation: a named type documents the test-data shape and catches invalid roles.

25. Read environment variables safelyReal-world

Read optional environment values with fallbacks and convert types.

Show solution
const env: Record<string, string | undefined> = { BASE_URL: 'https://qa.app.test', WORKERS: '4' };
const baseUrl: string = env.BASE_URL ?? 'http://localhost:3000';
const workers: number = Number(env.WORKERS ?? '2');
const debug: boolean = env.DEBUG === 'true';
console.log(baseUrl, workers, debug);

Output

https://qa.app.test 4 false

Explanation: environment values are string | undefined, so the types force you to handle missing values and convert them.

26. Timeouts as named constantsReal-world

Store timeouts in a typed constant object and calculate a total wait.

Show solution
const TIMEOUTS = { navigation: 30000, action: 10000, assertion: 5000 } as const;
type TimeoutName = keyof typeof TIMEOUTS;
const steps: TimeoutName[] = ['navigation', 'action', 'action', 'assertion'];
const worstCase = steps.reduce((sum, s) => sum + TIMEOUTS[s], 0);
console.log(`Worst case: ${worstCase / 1000}s`);

Output

Worst case: 55s

Explanation: keyof typeof builds a union of the object’s keys, so only real timeout names can be used.

27. Typed locator strategyReal-world

Model a locator as either a role or a test ID using a union of object types.

Show solution
type Locator = { by: 'role'; role: string; name: string } | { by: 'testId'; id: string };
const locators: Locator[] = [
  { by: 'role', role: 'button', name: 'Login' },
  { by: 'testId', id: 'cart-badge' }
];
for (const l of locators) {
  console.log(l.by === 'role' ? `getByRole('${l.role}', { name: '${l.name}' })` : `getByTestId('${l.id}')`);
}

Output

getByRole('button', { name: 'Login' })
getByTestId('cart-badge')

Explanation: checking by narrows the union, so TypeScript knows which properties exist in each branch.

28. Track state with typed let variablesReal-world

Track login attempts with typed state variables.

Show solution
const MAX_ATTEMPTS: number = 3;
let attempts: number = 0;
let locked: boolean = false;
const outcomes: boolean[] = [false, false, false];
for (const ok of outcomes) {
  if (ok) break;
  attempts++;
  locked = attempts >= MAX_ATTEMPTS;
}
console.log(`attempts=${attempts}, locked=${locked}`);

Output

attempts=3, locked=true

Explanation: explicit types make it clear which values change and what they may contain.

29. Build a typed report lineReal-world

Create typed variables for a test result and format a report line.

Show solution
type Result = { title: string; durationMs: number; status: 'passed' | 'failed' };
const result: Result = { title: 'Checkout with coupon', durationMs: 4210, status: 'passed' };
const line: string = `[${result.status.toUpperCase()}] ${result.title} - ${(result.durationMs / 1000).toFixed(1)}s`;
console.log(line);

Output

[PASSED] Checkout with coupon - 4.2s

Explanation: typed inputs guarantee that toUpperCase and toFixed are called on the right types.

30. Nullable variable for a found elementReal-world

Model a search result that may be null and handle both cases.

Show solution
const products = ['Mouse', 'Keyboard', 'Monitor'];
function findProduct(name: string): string | null {
  return products.find(p => p === name) ?? null;
}
for (const name of ['Keyboard', 'Webcam']) {
  const found: string | null = findProduct(name);
  console.log(found === null ? `${name} not found` : `Found ${found}`);
}

Output

Found Keyboard
Webcam not found

Explanation: with strictNullChecks, a string | null value must be checked before it is used as a string.

31. Variables typed from another variableReal-world

Use typeof to type a new object like an existing default config.

Show solution
const defaultUser = { name: 'QA User', role: 'viewer', active: true };
const admin: typeof defaultUser = { ...defaultUser, name: 'Admin', role: 'admin' };
console.log(admin);

Output

{ name: 'Admin', role: 'admin', active: true }

Explanation: typeof value in a type position copies the inferred type of an existing value.

Keep Practising

TypeScript for Playwright: Complete Beginner Tutorial (Setup, Concepts and Framework)

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 missing await on a typed value is flagged instantly.
  • Zero setup: Playwright runs .ts test 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:

ConceptWhere you use it in PlaywrightGuide
Variables and data typesTest data, URLs, timeoutsVariables, Data Types
Conditions and loopsData-driven tests, retriesConditions, Loops
Functions and async/awaitEvery test and helperFunctions
ArraysLists of elements and test casesArrays
ObjectsTest data, config, API payloadsObjects
Interfaces and typesTyped data and API responsesInterfaces
ClassesPage Object ModelClasses
GenericsFixtures, API helpersGenerics

πŸ’‘ 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:

TypeWhat it represents
PageA browser tab
LocatorA way to find element(s) on the page
BrowserContextAn isolated browser session (cookies, storage)
APIRequestContextThe client for API tests (request fixture)
TestInfoDetails about the running test (title, retries, attachments)
expectThe 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 messageCauseFix
Object is possibly ‘null’textContent() can return nullUse ?? '' or assert with toHaveText
Property ‘x’ does not exist on typeTypo or missing property in a typeFix the name or add it to the interface
‘await’ expressions are only allowed within async functionsMissing asyncAdd async before the function
Cannot find module ‘../pages/LoginPage’Wrong relative pathCheck the path and file name casing
Argument of type ‘string or undefined’Reading process.envProvide a default: process.env.BASE_URL ?? 'https://...'

πŸš€ Recommended Learning Path

  1. Learn the TypeScript basics from the table above (1 to 2 weeks).
  2. Write simple tests with locators and assertions: Locators, Assertions.
  3. Learn fixtures and annotations.
  4. Build a framework with the Page Object Model and external test data (JSON, CSV, .env).
  5. Complete the beginner automation project.
  6. 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

🎬 Watch the full TypeScript tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

TypeScript Generics Explained for Beginners (with Examples and Playwright Use Cases)

Generics sound advanced, but the idea is simple: write code once and let it work safely with any type. This guide explains TypeScript generics step by step for beginners, with clear examples and the places you already meet them in Playwright.

πŸ“– What You’ll Learn: what generics are, generic functions and arrow functions, multiple type parameters, constraints with extends and keyof, generic interfaces and classes, utility types like Partial and Omit, and generics in Playwright fixtures and API helpers.


πŸ§ͺ What Are Generics? (Explained Simply)

Generics let you write code that works with many types while still keeping full type safety. You write the logic once and leave a “blank” for the type. The blank is filled in when the code is used.

A useful analogy is a labelled storage box. The box works the same whether it holds books or shoes, but once you write “Books” on the label, you can only put books in it and you know you will get books out. In TypeScript that label is the type parameter, usually written <T>.


πŸ€” The Problem Generics Solve

Imagine a function that returns the first item of an array. Without generics you have two bad options:

// Option 1: one function per type (repetitive)
function firstString(items: string[]): string { return items[0]; }
function firstNumber(items: number[]): number { return items[0]; }

// Option 2: use any (no type safety)
function firstAny(items: any[]): any { return items[0]; }

const value = firstAny(['chromium', 'firefox']);
value.toFixed(2); // ❌ no error in the editor, but crashes at runtime!

✨ Your First Generic Function

Add <T> after the function name. T is a placeholder for “whatever type the caller uses”.

function first<T>(items: T[]): T {
  return items[0];
}

const browser = first(['chromium', 'firefox']); // T is string, so browser: string
const score = first([90, 85, 70]);              // T is number, so score: number

browser.toUpperCase(); // βœ… string method allowed
// browser.toFixed(2); // ❌ Error: toFixed does not exist on type string

TypeScript usually infers T from the arguments. You can also pass it explicitly: first<string>(['a', 'b']).

πŸ’‘ Tip: T is just a convention (for “Type”). Other common names are K (key), V (value) and E (element). Descriptive names like TData are fine too.


➑️ Generic Arrow Functions

Arrow functions use the same idea; the <T> goes before the parameters.

const last = <T>(items: T[]): T => items[items.length - 1];

const wrapInArray = <T>(value: T): T[] => [value];

last(['login', 'checkout', 'logout']); // 'logout'
wrapInArray(42);                       // [42]

πŸ“ Note: In .tsx files, write <T,> (with a comma) so the compiler does not confuse it with a JSX tag.


πŸ”’ Multiple Type Parameters

function pair<K, V>(key: K, value: V): [K, V] {
  return [key, value];
}

const p1 = pair('timeout', 30000); // [string, number]
const p2 = pair(1, true);          // [number, boolean]

function mapValues<T, R>(items: T[], fn: (item: T) => R): R[] {
  return items.map(fn);
}

const lengths = mapValues(['a', 'abc'], s => s.length); // number[]

🚧 Generic Constraints with extends

Sometimes T should not be “anything”. A constraint says “T must have at least these properties”. Use extends for that.

// T must have a length property
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

longest('playwright', 'ts');  // βœ… strings have length
longest([1, 2, 3], [1]);      // βœ… arrays have length
// longest(10, 20);           // ❌ Error: number has no length

A very common constraint is K extends keyof T, which means “K must be one of the property names of T”.

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: 'Deepesh', role: 'SDET', years: 8 };

getProperty(user, 'name');   // string
getProperty(user, 'years');  // number
// getProperty(user, 'email'); // ❌ Error: 'email' is not a key of user

πŸ“¦ Generic Interfaces and Types

Generics are especially powerful in interfaces. A classic example is a wrapper for API responses where only the data part changes.

interface ApiResponse<T> {
  status: number;
  message: string;
  data: T;
}

interface User { id: number; name: string }
interface Product { sku: string; price: number }

const userRes: ApiResponse<User> = {
  status: 200, message: 'OK', data: { id: 1, name: 'Asha' },
};

const productsRes: ApiResponse<Product[]> = {
  status: 200, message: 'OK', data: [{ sku: 'LP-01', price: 55000 }],
};

// Default type parameter
type Paginated<T = string> = { page: number; items: T[] };
const tags: Paginated = { page: 1, items: ['smoke', 'regression'] };

πŸ›οΈ Generic Classes

Classes can be generic too. This simple typed store keeps test data of one type only.

class DataStore<T> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  getAll(): T[] {
    return [...this.items];
  }

  find(predicate: (item: T) => boolean): T | undefined {
    return this.items.find(predicate);
  }
}

const users = new DataStore<{ email: string; active: boolean }>();
users.add({ email: 'a@test.com', active: true });
// users.add('hello'); // ❌ Error: string is not the stored type

const active = users.find(u => u.active);

🧰 Built-in Generic Utility Types

TypeScript ships with ready-made generics you will use every day:

UtilityWhat it doesExample
Array<T>Array of TArray<string>
Promise<T>Async result of TPromise<void>
Partial<T>All properties optionalOverride objects
Required<T>All properties requiredFinal config
Readonly<T>All properties read-onlyConstant test data
Pick<T, K>Keep only some propertiesPick<User, 'name'>
Omit<T, K>Remove some propertiesOmit<User, 'password'>
Record<K, V>Object with keys K and values VRecord<string, number>
interface User { id: number; name: string; email: string; password: string }

type PublicUser = Omit<User, 'password'>;        // no password
type UserUpdate = Partial<Pick<User, 'name' | 'email'>>;

const patch: UserUpdate = { email: 'new@test.com' };

🎭 Generics in Playwright: Real Examples

You already use generics in Playwright, maybe without noticing: every async method returns a Promise<T>, and test.extend<T>() is a generic.

1. A typed API helper

import { APIRequestContext, expect } from '@playwright/test';

export async function getJson<T>(request: APIRequestContext, url: string): Promise<T> {
  const response = await request.get(url);
  expect(response.ok()).toBeTruthy();
  return (await response.json()) as T;
}

// usage in a test
interface Todo { id: number; title: string; completed: boolean }

const todo = await getJson<Todo>(request, 'https://jsonplaceholder.typicode.com/todos/1');
expect(todo.completed).toBe(false); // todo is fully typed

2. Typed custom fixtures with test.extend<T>()

import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

type MyFixtures = {
  loginPage: LoginPage;
  testUser: { username: string; password: string };
};

export const test = base.extend<MyFixtures>({
  testUser: async ({}, use) => {
    await use({ username: 'standard_user', password: 'secret_sauce' });
  },
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
});

Learn more in Playwright Custom Fixtures with Examples.

3. A generic test data loader

import fs from 'fs';

export function loadJson<T>(path: string): T {
  return JSON.parse(fs.readFileSync(path, 'utf-8')) as T;
}

const users = loadJson<{ username: string; password: string }[]>('test-data/users.json');

See the full approach in Read JSON data in Playwright.


βœ… Best Practices

  • Use generics when the same logic works for different types; do not add <T> when one type is enough.
  • Let TypeScript infer T whenever possible; pass it explicitly when inference cannot know it (for example API responses).
  • Add constraints (extends) so the function only accepts types it can really handle.
  • Prefer generics over any. any switches off type checking; generics keep it on.
  • Remember that as T is a promise you make to the compiler; validate important API responses with assertions.

πŸ‹οΈ Practice Exercises

  1. Write a generic function lastItem<T>(arr: T[]): T | undefined.
  2. Write toMap<T, K extends keyof T>(items: T[], key: K) that returns a Map keyed by that property.
  3. Create interface Result<T> { ok: boolean; value?: T; error?: string } and a function that returns it.
  4. Build a generic Queue<T> class with enqueue, dequeue and size.

❓ FAQ

πŸ€” What are generics in TypeScript in simple terms?

A way to write a function, interface or class once and use it with any type, while TypeScript still tracks exactly which type is used each time.

πŸ€” What does <T> mean in TypeScript?

T is a type parameter: a placeholder name for a type that will be provided (or inferred) when the code is used.

πŸ€” What is the difference between generics and any?

any turns off type checking. Generics keep type checking: if you pass strings in, TypeScript knows you get strings out.

πŸ€” What does T extends mean in generics?

It is a constraint. T extends { id: number } means T can be any type, as long as it has a numeric id property.

πŸ€” Where are generics used in Playwright?

In Promise<T> return types, test.extend<Fixtures>() for custom fixtures, and typed helpers such as a getJson<T>() API function.


🎯 Next Steps

🎬 Watch the full TypeScript tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

✎ Practise what you learned: 30 TypeScript generics programs with solutions →

TypeScript Classes for Beginners: Constructors, Inheritance, Access Modifiers and Page Objects

Classes are the heart of object-oriented programming and the reason Playwright frameworks stay clean as they grow. This beginner guide explains TypeScript classes in plain language, from your first constructor to a working Page Object Model.

πŸ“– What You’ll Learn: class syntax, constructors, parameter properties, public/private/protected/readonly, inheritance with extends and super, getters and setters, static members, abstract classes, implementing interfaces, and Page Object Model classes in Playwright.


πŸ—οΈ What Is a Class?

A class is a template for creating objects. It bundles data (properties) and behavior (methods) together. Each object created from a class is called an instance.

A simple analogy: a class is the architect’s plan for a house, and each house built from that plan is an object. Every house has the same rooms (properties) and the same doors and switches (methods), but each one can have different paint and furniture (values).

In Playwright, classes are the foundation of the Page Object Model: each page of your application becomes a class with locators as properties and user actions as methods.


✍️ Creating Your First Class

class Car {
  brand: string;
  speed: number;

  constructor(brand: string, speed: number) {
    this.brand = brand;
    this.speed = speed;
  }

  describe(): string {
    return this.brand + ' runs at ' + this.speed + ' km/h';
  }
}

const car1 = new Car('Tata', 120);   // create an instance (object)
const car2 = new Car('Honda', 140);

console.log(car1.describe()); // Tata runs at 120 km/h
console.log(car2.brand);      // Honda

Key words to remember:

  • class declares the template.
  • constructor runs once when you call new and sets up the starting values.
  • this refers to the current object.
  • new creates a fresh object from the class.

⚠️ Common mistake: Forgetting this. inside a method is the most common beginner error. Write this.speed, not speed, to read the object’s own property.


⚑ Parameter Properties: The Short Way

TypeScript lets you declare and assign properties directly in the constructor by adding an access modifier (public, private, protected or readonly) before the parameter. You will see this in almost every Playwright page object.

// Long form
class UserLong {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

// Short form: same result, less code
class User {
  constructor(public name: string, private password: string) {}
}

const u = new User('admin', 'secret');
console.log(u.name);       // admin
// console.log(u.password); // ❌ Error: 'password' is private

πŸ” Access Modifiers: public, private, protected, readonly

Access modifiers control who can use a property or method.

ModifierAccessible fromTypical use
public (default)AnywhereMethods tests call, like login()
privateOnly inside the same classInternal helpers and secrets
protectedThe class and its child classesShared helpers in a base page
readonlyRead anywhere, set only in the constructorLocators, URLs
class BankAccount {
  readonly accountNumber: string;
  private balance = 0;

  constructor(accountNumber: string) {
    this.accountNumber = accountNumber;
  }

  deposit(amount: number): void {
    if (amount <= 0) throw new Error('Amount must be positive');
    this.balance += amount;
  }

  getBalance(): number {
    return this.balance;
  }
}

const acc = new BankAccount('SB-1001');
acc.deposit(500);
console.log(acc.getBalance()); // 500
// acc.balance = 1000000;      // ❌ Error: private

🧬 Inheritance with extends and super

Inheritance lets one class reuse another class. The child class gets every property and method of the parent and can add or override its own. Call super() in the child constructor to run the parent constructor first.

class Employee {
  constructor(public name: string, protected salary: number) {}

  getDetails(): string {
    return this.name + ' earns ' + this.salary;
  }
}

class Manager extends Employee {
  constructor(name: string, salary: number, public teamSize: number) {
    super(name, salary); // must come first
  }

  // Override the parent method
  getDetails(): string {
    return super.getDetails() + ' and manages ' + this.teamSize + ' people';
  }
}

const m = new Manager('Neha', 150000, 8);
console.log(m.getDetails()); // Neha earns 150000 and manages 8 people

🧰 Getters, Setters and Static Members

Getters and setters look like properties but run code. Static members belong to the class itself, not to each object. They are handy for shared constants and utility methods.

class Temperature {
  private celsius = 0;

  get fahrenheit(): number {
    return this.celsius * 9 / 5 + 32;
  }

  set value(c: number) {
    if (c < -273.15) throw new Error('Below absolute zero');
    this.celsius = c;
  }
}

const t = new Temperature();
t.value = 25;
console.log(t.fahrenheit); // 77

class TestUtils {
  static readonly DEFAULT_TIMEOUT = 30000;

  static randomEmail(): string {
    return 'user' + Date.now() + '@test.com';
  }
}

console.log(TestUtils.DEFAULT_TIMEOUT); // no "new" needed
console.log(TestUtils.randomEmail());

🧱 Abstract Classes

An abstract class cannot be created directly with new. It is a base for other classes and can force them to implement certain methods. This is useful for a BasePage that every page must extend.

abstract class Shape {
  abstract area(): number; // child classes MUST implement this

  describe(): string {
    return 'Area is ' + this.area().toFixed(2);
  }
}

class Circle extends Shape {
  constructor(private radius: number) { super(); }
  area(): number { return Math.PI * this.radius ** 2; }
}

console.log(new Circle(2).describe()); // Area is 12.57
// new Shape(); // ❌ Error: cannot create an instance of an abstract class

🀝 Classes and Interfaces Together

A class can implement an interface. The interface describes *what* must exist and the class provides *how* it works. If the class misses a member, TypeScript reports an error.

interface Reporter {
  report(testName: string, passed: boolean): void;
}

class ConsoleReporter implements Reporter {
  report(testName: string, passed: boolean): void {
    console.log((passed ? 'βœ… ' : '❌ ') + testName);
  }
}

Classes vs interfaces in one line: an interface is a compile-time contract with no code, while a class is real code that creates objects at runtime.


🎭 Classes in Playwright: Page Object Model

Here is a small but realistic Page Object Model built with everything you learned: readonly locators, a constructor, a protected base helper, inheritance and public action methods.

1. Base page

// pages/BasePage.ts
import { Page } from '@playwright/test';

export abstract class BasePage {
  constructor(protected readonly page: Page) {}

  abstract readonly path: string;

  async open(): Promise<void> {
    await this.page.goto('https://www.saucedemo.com' + this.path);
  }
}

2. Login page

// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';

export class LoginPage extends BasePage {
  readonly path = '/';
  readonly username: Locator;
  readonly password: Locator;
  readonly loginButton: Locator;
  readonly error: Locator;

  constructor(page: Page) {
    super(page);
    this.username = page.getByPlaceholder('Username');
    this.password = page.getByPlaceholder('Password');
    this.loginButton = page.getByRole('button', { name: 'Login' });
    this.error = page.locator('[data-test="error"]');
  }

  async login(user: string, pass: string): Promise<void> {
    await this.username.fill(user);
    await this.password.fill(pass);
    await this.loginButton.click();
  }
}

3. The test stays short and readable

// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

test('valid user can log in', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.open();
  await loginPage.login('standard_user', 'secret_sauce');
  await expect(page).toHaveURL(/inventory/);
});

test('locked user sees an error', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.open();
  await loginPage.login('locked_out_user', 'secret_sauce');
  await expect(loginPage.error).toContainText('locked out');
});

Go deeper with Playwright Page Object Model step by step and Playwright Custom Fixtures, which can create page objects for you automatically.


βœ… Best Practices

  • One class per page or component; keep each class focused on one responsibility (see SOLID principles with Playwright).
  • Mark locators readonly so they cannot be reassigned by accident.
  • Keep assertions in tests, and keep actions in page classes.
  • Use private for helpers the tests should not call directly.
  • Prefer composition (a page that *has* a header component) over deep inheritance chains.

❓ FAQ

πŸ€” What is the difference between a class and an object in TypeScript?

A class is the template; an object is a real value created from it with new. One class can create many objects.

πŸ€” What is a constructor in TypeScript?

A special method named constructor that runs automatically when you create an object with new. It sets the starting values of properties.

πŸ€” When should I use a class instead of an interface?

Use a class when you need real behavior and to create objects at runtime, such as page objects. Use an interface when you only need to describe a shape or contract.

πŸ€” Why do Playwright page objects use classes?

Classes group a page’s locators and actions in one place, so tests read like user steps and a UI change only needs one fix in the page class.

πŸ€” What does super() do?

In a child class constructor, super() calls the parent class constructor. It must run before you use this.


🎯 Next Steps

🎬 Watch the full TypeScript tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

✎ Practise what you learned: 30 TypeScript class programs with solutions →

TypeScript Interfaces Explained: Examples, Interface vs Type, and Playwright Use Cases

Interfaces are how TypeScript describes the shape of your data. In test automation they keep test data, API responses and page objects consistent, so typos are caught in the editor instead of in a failed CI run. This guide explains interfaces from scratch with simple examples.

πŸ“– What You’ll Learn: interface syntax, optional and readonly properties, default values, methods in interfaces, extends, index signatures, interface vs type, interface vs class, and how to use interfaces in Playwright tests.


πŸ“ What Is an Interface in TypeScript?

An interface is a named description of what an object must look like: which properties it has, what type each one is, and which methods it provides. Think of it as a contract or a blueprint. Any object that claims to follow the interface must match it, or TypeScript shows an error before your code runs.

Interfaces do not exist at runtime. They are removed when TypeScript compiles to JavaScript, so they add safety without adding any code to your tests.

interface User {
  username: string;
  password: string;
  age: number;
}

const tester: User = {
  username: 'standard_user',
  password: 'secret_sauce',
  age: 28,
};

// ❌ Error: Property 'age' is missing in type
// const broken: User = { username: 'x', password: 'y' };

πŸ’‘ Tip: By convention, interface names use PascalCase (User, LoginPage, ApiResponse). Do not add an I prefix like IUser; the TypeScript team recommends against it.


🧩 Interface Example: Optional and Readonly Properties

Just like object types, interface properties can be optional (?) or readonly.

interface TestEnvironment {
  readonly name: string;    // cannot change after creation
  baseUrl: string;
  apiKey?: string;          // optional
  timeout?: number;         // optional
}

const qa: TestEnvironment = { name: 'QA', baseUrl: 'https://qa.sqatools.in' };

qa.baseUrl = 'https://qa2.sqatools.in'; // βœ…
// qa.name = 'PROD';                    // ❌ read-only

🎚️ Default Values for Interface Properties

A common beginner question is “how do I give an interface a default value?”. Interfaces only describe types, so they cannot hold values. Instead, set defaults where the object is created, usually with the spread operator or default parameters.

interface BrowserOptions {
  browser: 'chromium' | 'firefox' | 'webkit';
  headless?: boolean;
  slowMo?: number;
}

const defaultOptions: Required<BrowserOptions> = {
  browser: 'chromium',
  headless: true,
  slowMo: 0,
};

function createOptions(custom: Partial<BrowserOptions> = {}): Required<BrowserOptions> {
  return { ...defaultOptions, ...custom };
}

createOptions({ headless: false });
// { browser: 'chromium', headless: false, slowMo: 0 }

πŸ“ Note: Partial<T> makes every property optional and Required<T> makes every property required. They are built-in utility types that work with any interface.


πŸ› οΈ Methods and Functions in Interfaces

Interfaces can describe methods too. This is exactly how you describe what a page object must be able to do.

interface LoginActions {
  open(): Promise<void>;
  login(username: string, password: string): Promise<void>;
  getErrorMessage(): Promise<string>;
}

// An interface can also describe a standalone function type
interface Formatter {
  (value: number): string;
}

const toPrice: Formatter = (value) => 'β‚Ή' + value.toFixed(2);
console.log(toPrice(499)); // β‚Ή499.00

πŸ”— Extending Interfaces

Interfaces can extend other interfaces to reuse properties. The new interface gets everything from the parent plus its own properties. You can extend more than one interface at once.

interface Person {
  firstName: string;
  lastName: string;
}

interface Employee extends Person {
  employeeId: number;
  department: string;
}

interface Timestamps {
  createdAt: Date;
}

interface Admin extends Employee, Timestamps {
  permissions: string[];
}

const lead: Admin = {
  firstName: 'Ravi',
  lastName: 'Kumar',
  employeeId: 101,
  department: 'QA',
  createdAt: new Date(),
  permissions: ['create-user', 'delete-user'],
};

πŸ“š Interfaces for Arrays, Nested Data and Dynamic Keys

interface Address { city: string; zip: string }

interface Customer {
  id: number;
  name: string;
  address: Address;          // nested interface
  tags: string[];            // array of strings
  orders: { id: number; total: number }[]; // array of objects
}

// Index signature: any number of string keys
interface Headers {
  [header: string]: string;
}

const headers: Headers = { 'Content-Type': 'application/json', Accept: '*/*' };

βš”οΈ Interface vs Type: What Is the Difference?

This is one of the most searched TypeScript questions. For describing objects, both work and look almost the same. The differences:

Featureinterfacetype
Describe object shapesβœ… Yesβœ… Yes
Extend / reuseextends keywordIntersection &
Union types ('a' | 'b')❌ Noβœ… Yes
Primitives and tuples❌ Noβœ… Yes
Declaration merging (same name twice)βœ… Merges❌ Error
Implemented by classesβœ… Yesβœ… Yes (object types)
// Same shape, two ways
interface UserA { name: string }
type UserB = { name: string };

// Extending
interface AdminA extends UserA { role: string }
type AdminB = UserB & { role: string };

// Only a type can do this
type Browser = 'chromium' | 'firefox' | 'webkit';
type Point = [number, number];

// Only an interface can do this (declaration merging)
interface Settings { theme: string }
interface Settings { language: string }
const s: Settings = { theme: 'dark', language: 'en' };

πŸ’‘ Tip: A simple rule for beginners: use interface for objects and class contracts (page objects, API models), and use type for unions, tuples and combinations. Be consistent within your project.


πŸ›οΈ Interface vs Class

An interface only describes shape and is removed at compile time. A class creates real objects at runtime and can contain code, constructors and default values. A class can implement an interface to promise it follows that contract.

interface Logger {
  log(message: string): void;
}

class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log('[test] ' + message);
  }
}

// ❌ Error if the class forgets the log() method

🎭 Interfaces in Playwright: Real Examples

1. Typing test data

// test-data/users.ts
export interface UserData {
  username: string;
  password: string;
  expectedPage: string;
}

export const users: Record<'standard' | 'problem', UserData> = {
  standard: { username: 'standard_user', password: 'secret_sauce', expectedPage: '/inventory.html' },
  problem:  { username: 'problem_user',  password: 'secret_sauce', expectedPage: '/inventory.html' },
};

2. Typing an API response

import { test, expect } from '@playwright/test';

interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}

test('GET a post returns the correct shape', async ({ request }) => {
  const response = await request.get('https://jsonplaceholder.typicode.com/posts/1');
  expect(response.ok()).toBeTruthy();

  const post: Post = await response.json();
  expect(post.id).toBe(1);
  expect(typeof post.title).toBe('string'); // editor autocompletes post.title
});

3. A contract for page objects

import { Page } from '@playwright/test';

interface BasePage {
  readonly page: Page;
  readonly url: string;
  open(): Promise<void>;
}

export class LoginPage implements BasePage {
  readonly url = 'https://www.saucedemo.com/';
  constructor(readonly page: Page) {}

  async open() {
    await this.page.goto(this.url);
  }

  async login(user: { username: string; password: string }) {
    await this.page.getByPlaceholder('Username').fill(user.username);
    await this.page.getByPlaceholder('Password').fill(user.password);
    await this.page.getByRole('button', { name: 'Login' }).click();
  }
}

Read the full pattern in Playwright Page Object Model step by step.


βœ… Best Practices

  • Name interfaces after what they represent: UserData, ApiResponse, CheckoutPage.
  • Keep shared interfaces in one folder (for example types/) and export them.
  • Use extends to build specific interfaces from general ones instead of copy-pasting properties.
  • Type every API response you assert on. Autocomplete then prevents typos in property names.
  • Prefer Partial<T> for “update” or “override” objects instead of making every property optional.

❓ FAQ

πŸ€” What is an interface in TypeScript in simple words?

It is a named blueprint that lists the properties and methods an object must have. TypeScript checks every object against it while you write code.

πŸ€” Should I use interface or type in TypeScript?

Both work for objects. Use interface for object shapes and class contracts, and type for unions, tuples and other combinations. Pick one style per project and stay consistent.

πŸ€” Can a TypeScript interface have default values?

No. Interfaces only describe types. Set defaults when creating the object, for example const opts = { ...defaults, ...custom }.

πŸ€” Can an interface extend multiple interfaces?

Yes: interface Admin extends Employee, Timestamps {} combines all properties from both parents.

πŸ€” Do interfaces slow down my tests?

No. Interfaces are removed during compilation and produce zero JavaScript, so they have no runtime cost.


🎯 Next Steps

🎬 Watch the full TypeScript tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

✎ Practise what you learned: 30 TypeScript interface programs with solutions →

TypeScript Objects: Complete Beginner Guide with Playwright Examples

Objects are the backbone of test automation: login credentials, form data, API payloads and even your Playwright config are all objects. In this beginner-friendly guide you will learn how TypeScript objects work, how to give them types, and how to use them in real Playwright tests.

πŸ“– What You’ll Learn: object types, type aliases, optional and readonly properties, index signatures and Record, nested objects, arrays of objects, looping, destructuring, merging, comparing objects, and objects in Playwright tests.


🧱 What Is an Object in TypeScript?

An object is a way to group related values together under one name. Instead of creating five separate variables for a user’s name, email, password, role and age, you put them in one object. Each value is stored as a property: a key (the name) and a value.

In plain JavaScript, objects can hold anything and you only find mistakes when the code runs. TypeScript adds types to objects, so your editor warns you the moment you misspell a property or put a number where a string belongs. For test automation this matters a lot, because most test data, such as login users, form inputs and API payloads, is stored in objects.

// A simple object: TypeScript infers the type automatically
const user = {
  name: 'Deepesh',
  email: 'deepesh@sqatools.in',
  age: 30,
  isActive: true,
};

console.log(user.name);   // Deepesh
console.log(user['email']); // deepesh@sqatools.in (bracket notation)

πŸ“ Note: When you hover over user in VS Code, TypeScript shows the inferred type { name: string; email: string; age: number; isActive: boolean }. You did not write it, but TypeScript still protects you.


✍️ Declaring an Object Type

You can describe the shape of an object explicitly. This is called an object type annotation. It lists every property and its type, separated by semicolons.

let loginUser: { username: string; password: string; rememberMe: boolean };

loginUser = {
  username: 'standard_user',
  password: 'secret_sauce',
  rememberMe: false,
};

// ❌ Error: Property 'rememberMe' is missing
// loginUser = { username: 'admin', password: '1234' };

// ❌ Error: Type 'number' is not assignable to type 'string'
// loginUser = { username: 101, password: 'x', rememberMe: true };

Writing the shape inline works, but it gets repetitive. In real projects you give the shape a name with a type alias (or an interface) and reuse it everywhere.

type User = {
  username: string;
  password: string;
  role: string;
};

const admin: User = { username: 'admin', password: 'Admin@123', role: 'admin' };
const guest: User = { username: 'guest', password: 'Guest@123', role: 'viewer' };

❓ Optional and Readonly Properties

Not every property is always present. Add a ? after the name to make it optional. Add readonly in front to stop anyone from changing it after the object is created.

type TestConfig = {
  readonly baseUrl: string; // cannot be changed later
  browser: string;
  headless?: boolean;       // optional
  retries?: number;         // optional
};

const config: TestConfig = {
  baseUrl: 'https://sqatools.in',
  browser: 'chromium',
};

config.browser = 'firefox';   // βœ… allowed
// config.baseUrl = 'https://x.com'; // ❌ Error: read-only property

// Optional values may be undefined, so give a fallback
const retries = config.retries ?? 0;

πŸ’‘ Tip: Use readonly for values like URLs and environment names that should never change during a test run. It prevents accidental overwrites in shared test data.


πŸ”§ Adding, Updating and Deleting Properties

TypeScript only lets you use properties that exist in the type. This is a feature: it catches typos like usernmae immediately.

type Product = { name: string; price: number; inStock?: boolean };

const product: Product = { name: 'Laptop', price: 55000 };

product.price = 52000;     // βœ… update
product.inStock = true;    // βœ… add an optional property
delete product.inStock;    // βœ… delete works only on optional properties

// product.color = 'black'; // ❌ Error: 'color' does not exist on type 'Product'

If you genuinely need an object whose keys are not known in advance (for example, HTTP headers), use an index signature or the built-in Record type.

// Any string key, every value is a string
const headers: { [key: string]: string } = {};
headers['Content-Type'] = 'application/json';
headers['Authorization'] = 'Bearer token123';

// Same thing using Record<KeyType, ValueType>
const statusMessages: Record<number, string> = {
  200: 'OK',
  404: 'Not Found',
  500: 'Server Error',
};

πŸͺ† Nested Objects

Objects can contain other objects. This is common for API responses and page configuration.

type Address = { city: string; pincode: string };

type Customer = {
  id: number;
  name: string;
  address: Address;          // nested object
  contacts: { email: string; phone?: string };
};

const customer: Customer = {
  id: 1,
  name: 'Asha',
  address: { city: 'Pune', pincode: '411001' },
  contacts: { email: 'asha@test.com' },
};

console.log(customer.address.city);      // Pune
console.log(customer.contacts.phone?.length); // undefined (optional chaining)

⚠️ Common mistake: Accessing a property of something that may be undefined crashes at runtime. Use optional chaining ?. to read safely, as in customer.contacts.phone?.length.


πŸ“‹ Arrays of Objects

An array of objects is the most common way to store test data for data-driven tests. Each object is one test case.

type LoginCase = { title: string; username: string; password: string; expectError: boolean };

const loginCases: LoginCase[] = [
  { title: 'valid user',      username: 'standard_user',  password: 'secret_sauce', expectError: false },
  { title: 'locked user',     username: 'locked_out_user', password: 'secret_sauce', expectError: true },
  { title: 'wrong password',  username: 'standard_user',  password: 'wrong',        expectError: true },
];

// Find, filter and map work exactly like normal arrays
const negativeCases = loginCases.filter(c => c.expectError);
const titles = loginCases.map(c => c.title);
console.log(titles); // ['valid user', 'locked user', 'wrong password']

Learn more array methods in our TypeScript Arrays guide.


πŸ” Looping Through an Object

Use Object.keys(), Object.values() and Object.entries() to work with every property.

const scores = { chromium: 98, firefox: 95, webkit: 91 };

Object.keys(scores);    // ['chromium', 'firefox', 'webkit']
Object.values(scores);  // [98, 95, 91]

for (const [browser, score] of Object.entries(scores)) {
  console.log(browser + ' scored ' + score);
}

🧩 Destructuring, Spread and Merging Objects

Destructuring pulls properties out into variables. The spread operator ... copies properties into a new object, which is the easiest way to merge or override values.

const user = { username: 'admin', password: 'Admin@123', role: 'admin' };

// Destructuring
const { username, password } = user;

// Merge two objects (later values win)
const defaults = { browser: 'chromium', headless: true, retries: 0 };
const overrides = { headless: false, retries: 2 };
const finalConfig = { ...defaults, ...overrides };
// { browser: 'chromium', headless: false, retries: 2 }

// Copy and change one field without touching the original
const guest = { ...user, username: 'guest', role: 'viewer' };

⚠️ Common mistake: Spread makes a shallow copy. Nested objects are still shared between the copy and the original. For a deep copy use structuredClone(obj).


βš–οΈ Comparing Objects

Two objects are only === equal if they are the same object in memory, even if their contents match. To compare contents, compare the values or use a deep equality check.

const a = { id: 1, name: 'Test' };
const b = { id: 1, name: 'Test' };

console.log(a === b); // false: different objects
console.log(JSON.stringify(a) === JSON.stringify(b)); // true (simple objects, same key order)

In Playwright tests you do not need any of this: expect(actual).toEqual(expected) performs a deep comparison for you.


🎭 Objects in Playwright: Real Examples

Objects are everywhere in Playwright. The config file is an object, test data is usually objects, and API requests send objects as JSON.

1. Test data object for a login test

import { test, expect } from '@playwright/test';

type Credentials = { username: string; password: string };

const validUser: Credentials = { username: 'standard_user', password: 'secret_sauce' };

test('user can log in', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await page.getByPlaceholder('Username').fill(validUser.username);
  await page.getByPlaceholder('Password').fill(validUser.password);
  await page.getByRole('button', { name: 'Login' }).click();
  await expect(page).toHaveURL(/inventory/);
});

2. Data-driven tests from an array of objects

const cases = [
  { username: 'locked_out_user', error: 'Sorry, this user has been locked out.' },
  { username: 'unknown_user',    error: 'Username and password do not match' },
];

for (const c of cases) {
  test('shows error for ' + c.username, async ({ page }) => {
    await page.goto('https://www.saucedemo.com/');
    await page.getByPlaceholder('Username').fill(c.username);
    await page.getByPlaceholder('Password').fill('secret_sauce');
    await page.getByRole('button', { name: 'Login' }).click();
    await expect(page.locator('[data-test="error"]')).toContainText(c.error);
  });
}

3. Sending and checking an object in an API test

test('create a post via API', async ({ request }) => {
  const payload = { title: 'Playwright', body: 'Objects in TypeScript', userId: 1 };

  const response = await request.post('https://jsonplaceholder.typicode.com/posts', { data: payload });
  expect(response.status()).toBe(201);

  const body = await response.json();
  expect(body).toMatchObject(payload); // deep, partial object comparison
});

βœ… Best Practices

  • Give every reusable object shape a name with type or interface instead of repeating inline types.
  • Mark values that must not change as readonly.
  • Use optional properties ? only when a value can truly be missing, and handle undefined with ?? or ?..
  • Keep test data objects in a separate file (for example test-data/users.ts) so tests stay short.
  • Prefer toEqual or toMatchObject in Playwright assertions to compare objects.

❓ FAQ

πŸ€” What is the difference between an object type and an interface in TypeScript?

Both describe the shape of an object and are often interchangeable. Interfaces can be extended and merged, while type aliases can also describe unions and primitives. Read the full comparison in TypeScript Interfaces.

πŸ€” How do I define an array of objects in TypeScript?

Create a type for one item, then add []: type User = { name: string }; const users: User[] = [];. You can also write Array<User>.

πŸ€” How do I merge two objects in TypeScript?

Use the spread operator: const merged = { ...first, ...second };. Properties in second overwrite the same properties in first.

πŸ€” How do I make an object property optional?

Add a question mark after the property name, for example phone?: string. Its value can then be a string or undefined.

πŸ€” Why does TypeScript say “Property does not exist on type”?

You are using a property that is not part of the object’s type, usually because of a typo or a missing field in the type. Add it to the type (optionally with ?) or fix the spelling.


🎯 Next Steps

🎬 Watch the full TypeScript tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

✎ Practise what you learned: 31 TypeScript object programs with solutions →

TypeScript Arrays β€” Complete Guide with map, filter, reduce & Playwright Examples

Arrays are everywhere in test automation β€” storing test data, collecting page elements, managing results. TypeScript arrays come with strong typing that catches bugs before your tests even run. This guide covers everything from basics to advanced patterns with Playwright examples.

πŸ“– What You’ll Learn: Array types, push/pop/splice, map/filter/reduce, spread operator, tuples, readonly arrays, and array operations in Playwright tests.


πŸ“¦ Declaring Arrays

// Two ways to declare typed arrays
let browsers: string[] = ['chromium', 'firefox', 'webkit'];
let scores: Array<number> = [85, 92, 78, 95];

// Mixed types with union
let mixed: (string | number)[] = ['hello', 42, 'world'];

// Empty array with type
let results: boolean[] = [];

// Array of objects
let users: { name: string; role: string }[] = [
  { name: 'Deepesh', role: 'SDET' },
  { name: 'John', role: 'QA Lead' },
];

// 2D array (matrix)
let grid: number[][] = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
];

βž• Adding & Removing Elements

let fruits: string[] = ['apple', 'banana'];

// Add to end
fruits.push('cherry');           // ['apple', 'banana', 'cherry']

// Add to beginning
fruits.unshift('mango');         // ['mango', 'apple', 'banana', 'cherry']

// Remove from end
let last = fruits.pop();         // 'cherry', array: ['mango', 'apple', 'banana']

// Remove from beginning
let first = fruits.shift();      // 'mango', array: ['apple', 'banana']

// Remove/insert at position with splice
let colors: string[] = ['red', 'green', 'blue', 'yellow'];
colors.splice(1, 2);             // Remove 2 items from index 1 β†’ ['red', 'yellow']
colors.splice(1, 0, 'purple');   // Insert at index 1 β†’ ['red', 'purple', 'yellow']

// Find index and remove
let items = ['a', 'b', 'c', 'd'];
let idx = items.indexOf('c');
if (idx !== -1) items.splice(idx, 1);  // ['a', 'b', 'd']

πŸ” Searching & Checking Arrays

let numbers: number[] = [10, 20, 30, 40, 50];

// includes β€” does it contain a value?
console.log(numbers.includes(30));  // true

// indexOf β€” find position (-1 if not found)
console.log(numbers.indexOf(40));   // 3

// find β€” first element matching condition
let found = numbers.find(n => n > 25);
console.log(found);  // 30

// findIndex β€” index of first match
let foundIdx = numbers.findIndex(n => n > 25);
console.log(foundIdx);  // 2

// every β€” do ALL elements match?
let allPositive = numbers.every(n => n > 0);    // true

// some β€” does ANY element match?
let hasLarge = numbers.some(n => n > 100);       // false

πŸ”„ map, filter & reduce

let prices: number[] = [100, 200, 300, 400, 500];

// map β€” transform each element
let discounted = prices.map(p => p * 0.9);
console.log(discounted);  // [90, 180, 270, 360, 450]

// filter β€” keep elements matching condition
let expensive = prices.filter(p => p >= 300);
console.log(expensive);  // [300, 400, 500]

// reduce β€” combine all elements into one value
let total = prices.reduce((sum, p) => sum + p, 0);
console.log(total);  // 1500

// Chaining β€” filter + map + reduce
let premiumTotal = prices
  .filter(p => p >= 300)           // [300, 400, 500]
  .map(p => p * 1.1)               // [330, 440, 550]
  .reduce((sum, p) => sum + p, 0); // 1320
console.log(premiumTotal);

// flat β€” flatten nested arrays
let nested: number[][] = [[1, 2], [3, 4], [5]];
let flat = nested.flat();
console.log(flat);  // [1, 2, 3, 4, 5]

πŸ’‘ map transforms, filter selects, reduce combines. Chain them together for powerful data pipelines. These are the three most important array methods to master.


πŸ“Œ Spread, Destructuring & Tuples

// Spread operator β€” copy and merge arrays
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let combined = [...arr1, ...arr2];    // [1, 2, 3, 4, 5, 6]
let copy = [...arr1];                 // [1, 2, 3] β€” independent copy

// Destructuring β€” extract values
let [first, second, ...rest] = [10, 20, 30, 40, 50];
console.log(first);  // 10
console.log(rest);   // [30, 40, 50]

// Tuple β€” fixed-length, typed array
let coord: [number, number] = [40.7128, -74.0060];
let entry: [string, number, boolean] = ['test-1', 95, true];

// Readonly array β€” cannot be modified
const readonlyBrowsers: readonly string[] = ['chromium', 'firefox', 'webkit'];
// readonlyBrowsers.push('edge');  // ❌ Error! Cannot modify

// Sorting
let nums = [30, 10, 50, 20, 40];
nums.sort((a, b) => a - b);          // [10, 20, 30, 40, 50] ascending
nums.sort((a, b) => b - a);          // [50, 40, 30, 20, 10] descending

🎭 Arrays in Playwright β€” Real Examples

import { test, expect } from '@playwright/test';

test('collect and verify all product names', async ({ page }) => {
  await page.goto('https://sqatools.in');
  
  // Collect all text content into an array
  const links = page.locator('nav a');
  const texts: string[] = await links.allTextContents();
  
  console.log('Navigation links:', texts);
  expect(texts.length).toBeGreaterThan(0);
  
  // Filter only links containing 'Playwright'
  const playwrightLinks = texts.filter(t => t.toLowerCase().includes('playwright'));
  console.log('Playwright links:', playwrightLinks);
});

// Data-driven testing with arrays
const testData: [string, string, boolean][] = [
  ['admin@test.com', 'admin123', true],
  ['invalid@test.com', 'wrong', false],
  ['', '', false],
];

for (const [email, password, shouldPass] of testData) {
  test(`login with ${email || 'empty'} should ${shouldPass ? 'pass' : 'fail'}`, async ({ page }) => {
    await page.goto('/login');
    await page.fill('#email', email);
    await page.fill('#password', password);
    await page.click('#submit');
    
    if (shouldPass) {
      await expect(page.locator('.dashboard')).toBeVisible();
    } else {
      await expect(page.locator('.error')).toBeVisible();
    }
  });
}

❓ FAQ

πŸ€” What is the difference between string[] and Array<string>?

They are identical β€” both declare an array of strings. string[] is shorter and more commonly used. Array<string> is clearer for complex types like Array<Map<string, number>>.


πŸ€” How do I get all text from Playwright locators into an array?

Use await locator.allTextContents() β€” it returns a string[] with the text content of every matching element. For inner text, use allInnerTexts().

🎯 Next Steps

🎬 Watch the full TypeScript tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

✎ Practise what you learned: 31 TypeScript array and tuple programs with solutions →

TypeScript Functions β€” Declarations, Arrow Functions, Async & Generics with Playwright

Functions are the building blocks of clean, reusable code. In test automation, you’ll use them everywhere β€” from page actions to custom assertions to utility helpers. This guide covers every function pattern in TypeScript with real Playwright examples.

πŸ“– What You’ll Learn: Function declarations, arrow functions, optional & default parameters, rest parameters, return types, overloads, generics in functions, and async functions in Playwright.


πŸ”§ Function Declarations & Expressions

// Function declaration
function greet(name: string): string {
  return `Hello, ${name}!`;
}

// Function expression
const add = function(a: number, b: number): number {
  return a + b;
};

// Arrow function (most common in modern TypeScript)
const multiply = (a: number, b: number): number => a * b;

// Void function β€” returns nothing
function logMessage(msg: string): void {
  console.log(`[LOG]: ${msg}`);
}

console.log(greet('QA Engineer'));  // Hello, QA Engineer!
console.log(add(5, 3));            // 8
console.log(multiply(4, 7));       // 28

βš™οΈ Optional, Default & Rest Parameters

// Optional parameter (use ?)
function createUser(name: string, email?: string): string {
  return email ? `${name} (${email})` : name;
}
console.log(createUser('Deepesh'));                    // Deepesh
console.log(createUser('Deepesh', 'test@test.com'));   // Deepesh (test@test.com)

// Default parameter
function wait(ms: number = 1000): void {
  console.log(`Waiting ${ms}ms...`);
}
wait();      // Waiting 1000ms...
wait(3000);  // Waiting 3000ms...

// Rest parameters (variable number of args)
function logAll(...messages: string[]): void {
  messages.forEach((msg, i) => console.log(`${i + 1}. ${msg}`));
}
logAll('Step 1', 'Step 2', 'Step 3');

// Destructured parameters
function printConfig({ url, timeout = 30000 }: { url: string; timeout?: number }): void {
  console.log(`URL: ${url}, Timeout: ${timeout}`);
}
printConfig({ url: 'https://sqatools.in' });

πŸ“ Function Types & Callbacks

// Type alias for a function
type Validator = (value: string) => boolean;

const isEmail: Validator = (value) => value.includes('@');
const isNotEmpty: Validator = (value) => value.trim().length > 0;

console.log(isEmail('test@test.com'));  // true
console.log(isNotEmpty(''));            // false

// Callback functions
function runValidation(input: string, validator: Validator, onResult: (passed: boolean) => void): void {
  const result = validator(input);
  onResult(result);
}

runValidation('hello@test.com', isEmail, (passed) => {
  console.log(passed ? 'βœ… Valid' : '❌ Invalid');
});

// Returning functions (higher-order)
function createMultiplier(factor: number): (n: number) => number {
  return (n) => n * factor;
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5));  // 10
console.log(triple(5));  // 15

🧬 Generic Functions

// Generic function β€” works with any type
function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

console.log(firstElement([1, 2, 3]));          // 1 (type: number)
console.log(firstElement(['a', 'b', 'c']));    // 'a' (type: string)

// Generic with constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: 'Deepesh', role: 'SDET' };
console.log(getProperty(user, 'name'));  // 'Deepesh'

// Generic async function
async function fetchData<T>(url: string): Promise<T> {
  const response = await fetch(url);
  return response.json() as Promise<T>;
}

πŸ’‘ Generics let you write ONE function that works with strings, numbers, objects β€” anything. TypeScript figures out the type automatically when you call it.


⚑ Async Functions

// Async function returns a Promise
async function fetchTitle(url: string): Promise<string> {
  const response = await fetch(url);
  const html = await response.text();
  const match = html.match(/<title>(.*)<\/title>/);
  return match ? match[1] : 'No title found';
}

// Async arrow function
const delay = async (ms: number): Promise<void> => {
  return new Promise(resolve => setTimeout(resolve, ms));
};

// Try-catch in async functions
async function safeRequest(url: string): Promise<string | null> {
  try {
    const response = await fetch(url);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return await response.text();
  } catch (error) {
    console.error('Request failed:', error);
    return null;
  }
}

🎭 Functions in Playwright β€” Real Examples

import { test, expect, Page } from '@playwright/test';

// Reusable login helper function
async function login(page: Page, email: string, password: string): Promise<void> {
  await page.goto('/login');
  await page.fill('#email', email);
  await page.fill('#password', password);
  await page.click('#login-btn');
  await expect(page.locator('.dashboard')).toBeVisible();
}

// Retry function for flaky operations
async function retry<T>(fn: () => Promise<T>, maxAttempts: number = 3): Promise<T> {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxAttempts) throw error;
      console.log(`Attempt ${attempt} failed, retrying...`);
      await new Promise(r => setTimeout(r, 1000));
    }
  }
  throw new Error('Should not reach here');
}

// Using helper functions in tests
test('login and verify dashboard', async ({ page }) => {
  await login(page, 'admin@sqatools.in', 'test123');
  await expect(page).toHaveTitle(/Dashboard/);
});

test('retry flaky element click', async ({ page }) => {
  await page.goto('https://sqatools.in');
  await retry(async () => {
    await page.click('.dynamic-button');
    await expect(page.locator('.result')).toBeVisible();
  });
});

❓ FAQ

πŸ€” When should I use arrow functions vs regular functions?

Use arrow functions for callbacks, short expressions, and class methods. Use regular functions when you need hoisting or a named function in stack traces. In Playwright, arrow functions are standard for test callbacks and helpers.


πŸ€” What is the difference between optional and default parameters?

Optional parameters (name?: string) can be omitted and will be undefined. Default parameters (name: string = 'Guest') have a fallback value when omitted. Default parameters are preferred when you have a sensible default.

🎯 Next Steps

🎬 Watch the full TypeScript tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

✎ Practise what you learned: 31 TypeScript function programs with solutions →

TypeScript Loops β€” for, while, for…of & forEach with Playwright Examples

Loops let you repeat actions β€” whether it’s iterating through test data, checking multiple elements, or retrying flaky operations. This guide covers every loop type in TypeScript with real Playwright examples you’ll use daily in test automation.

πŸ“– What You’ll Learn: for loop, while loop, do…while, for…of, for…in, forEach, break/continue, and looping through Playwright elements.


πŸ” for Loop

The classic loop β€” you control the start, end, and step.

// Basic for loop
for (let i: number = 0; i < 5; i++) {
  console.log('Iteration:', i);
}
// Output: 0, 1, 2, 3, 4

// Counting backwards
for (let i: number = 10; i > 0; i -= 2) {
  console.log(i);
}
// Output: 10, 8, 6, 4, 2

// Looping through an array by index
let browsers: string[] = ['chromium', 'firefox', 'webkit'];
for (let i = 0; i < browsers.length; i++) {
  console.log(`Browser ${i + 1}: ${browsers[i]}`);
}

πŸ”„ while and do...while Loops

// while β€” checks condition BEFORE each iteration
let attempts: number = 0;
while (attempts < 3) {
  console.log(`Attempt ${attempts + 1}`);
  attempts++;
}

// do...while β€” runs at least ONCE, then checks condition
let input: string = '';
do {
  input = 'valid';  // simulating user input
  console.log('Processing input:', input);
} while (input === '');

// Retry pattern (common in test automation)
let retryCount: number = 0;
let success: boolean = false;
while (!success && retryCount < 3) {
  try {
    // attempt some operation
    success = true;
    console.log('Operation succeeded!');
  } catch {
    retryCount++;
    console.log(`Retry ${retryCount}/3`);
  }
}

⚠️ Always include a way to exit while loops (increment a counter, set a flag). An infinite loop will crash your tests!


🎯 for...of and for...in Loops

// for...of β€” iterate over VALUES (arrays, strings, maps)
let testCases: string[] = ['login', 'signup', 'checkout'];
for (const tc of testCases) {
  console.log('Running test:', tc);
}

// for...in β€” iterate over KEYS/INDICES (objects)
let config = { baseURL: 'https://sqatools.in', timeout: 30000, retries: 2 };
for (const key in config) {
  console.log(`${key}: ${config[key as keyof typeof config]}`);
}

// Iterating over a Map
let testData = new Map();
testData.set('user1', 'admin@test.com');
testData.set('user2', 'user@test.com');

for (const [key, value] of testData) {
  console.log(`${key} -> ${value}`);
}

πŸ’‘ Quick Rule: Use for...of for arrays and iterable values. Use for...in for object keys. Never use for...in on arrays β€” it iterates over indices as strings, which causes bugs.


πŸ“‹ forEach and Array Methods

let scores: number[] = [85, 92, 78, 95, 88];

// forEach β€” run a function for each element
scores.forEach((score, index) => {
  console.log(`Student ${index + 1}: ${score}`);
});

// map β€” transform each element into a new array
let grades = scores.map(s => s >= 90 ? 'A' : s >= 80 ? 'B' : 'C');
console.log(grades);  // ['B', 'A', 'C', 'A', 'B']

// filter β€” keep only elements that match
let topScores = scores.filter(s => s >= 90);
console.log(topScores);  // [92, 95]

// find β€” get the first match
let firstTop = scores.find(s => s >= 90);
console.log(firstTop);  // 92

// every/some β€” check all or any
let allPassed = scores.every(s => s >= 60);   // true
let anyPerfect = scores.some(s => s === 100); // false

⏹️ break and continue

// break β€” exit the loop entirely
for (let i = 0; i < 10; i++) {
  if (i === 5) break;
  console.log(i);
}
// Output: 0, 1, 2, 3, 4

// continue β€” skip current iteration, go to next
for (let i = 0; i < 10; i++) {
  if (i % 2 === 0) continue;  // skip even numbers
  console.log(i);
}
// Output: 1, 3, 5, 7, 9

🎭 Loops in Playwright β€” Real Examples

import { test, expect } from '@playwright/test';

test('iterate through page elements', async ({ page }) => {
  await page.goto('https://sqatools.in');
  
  // Get all navigation links and verify each one
  const navLinks = page.locator('nav a');
  const count = await navLinks.count();
  
  for (let i = 0; i < count; i++) {
    const text = await navLinks.nth(i).textContent();
    const href = await navLinks.nth(i).getAttribute('href');
    console.log(`Link ${i + 1}: ${text} -> ${href}`);
    expect(href).not.toBeNull();
  }
});

// Data-driven test with for...of
const testUsers = [
  { email: 'admin@test.com', role: 'admin' },
  { email: 'user@test.com', role: 'user' },
  { email: 'guest@test.com', role: 'guest' },
];

for (const user of testUsers) {
  test(`login as ${user.role}`, async ({ page }) => {
    await page.goto('/login');
    await page.fill('#email', user.email);
    await page.fill('#password', 'test123');
    await page.click('#login-btn');
    await expect(page.locator('.role-badge')).toHaveText(user.role);
  });
}

❓ FAQ

πŸ€” What is the difference between for...of and for...in?

for...of iterates over values (arrays, strings). for...in iterates over keys/indices (objects). Use for...of for arrays and for...in for object properties.


πŸ€” How do I loop through Playwright locators?

Use locator.count() to get the number of matches, then use a for loop with locator.nth(i) to access each element individually.

🎯 Next Steps

🎬 Watch the full TypeScript tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

✎ Practise what you learned: 30 TypeScript loop programs with solutions →