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
typeorinterfaceinstead of repeating inline types. - Mark values that must not change as
readonly. - Use optional properties
?only when a value can truly be missing, and handleundefinedwith??or?.. - Keep test data objects in a separate file (for example
test-data/users.ts) so tests stay short. - Prefer
toEqualortoMatchObjectin 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
- 📘 TypeScript Interfaces: define reusable object shapes
- 📘 TypeScript Classes: build objects with behavior
- 📘 TypeScript Arrays
- 📘 TypeScript Functions
- 🎭 Playwright with TypeScript Training Notes: the full course roadmap
🎬 Watch the full TypeScript tutorial series on YouTube!
Subscribe to SqaToolsByDY • Practice on sqatools.in