🔀 How to Master Fundamental TypeScript Logic for Smarter Automation QA


In our last article, we organized our test data into collections with Arrays and structured it with Objects. We built a clean toolbox. Now, it's time to add power tools.
This article is about breathing life into your scripts. It's about writing code that can repeat actions, make decisions, and be reused. We'll focus heavily on a core principle of software development: DRY (Don't Repeat Yourself). Writing the same code over and over is slow, error-prone, and a nightmare to maintain.
We'll tackle this by mastering TypeScript's fundamental logic: Functions, Loops, and Conditionals. By the end, you'll be able to write automation that isn't just a series of steps, but an intelligent, robust system.
✅ Prerequisites
This article assumes you are comfortable with the topics from our previous discussion:
Basic TypeScript types (
string
,number
,boolean
)Structuring data with
Arrays
andObjects
If you've got that down, you're ready to make your code smart.
📦 Defining Functions: Your Reusable Code Blocks
A function is a reusable block of code that performs a specific task. If you find yourself writing the same three lines of code in multiple tests, that's a perfect candidate for a function. This is the heart of the DRY principle.
In TypeScript, you'll primarily see two styles: named functions and arrow functions.
Named Functions
This is the classic, most recognizable function declaration.
// A named function for a common user action
async function navigateToHomePage() {
await page.goto('https://idavidov.eu/');
await expect(page.getByRole('heading', { name: 'Intelligent Quality' })).toBeVisible();
}
// You can now "call" this function anywhere you need it
await navigateToHomePage();
Arrow Functions
Arrow functions are a more modern, concise syntax, especially popular in modern JavaScript and TypeScript.
// The same function as an arrow function
const navigateToHomePageArrow = async () => {
await page.goto('https://idavidov.eu/');
await expect(page.getByRole('heading', { name: 'Intelligent Quality' })).toBeVisible();
};
// Calling it is exactly the same
await navigateToHomePageArrow();
For most QA automation, the choice is a matter of team style. Both achieve the same goal: making your code reusable.
⚙️ Advanced Function Parameters: More Power, Less Code
Now let's make our functions more flexible and powerful.
Object Destructuring
Passing entire objects to functions is great, but sometimes the function only needs a few properties from that object. Object destructuring lets you pull out the exact properties you need, right in the parameter list.
Imagine you have a user
object. Instead of passing the whole object and then accessing user.username
, you can destructure it.
// The function expects an object with username and password
async function login({ username, password }: User) {
await page.getByLabel('Username').fill(username);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Log In' }).click();
}
// Now you can pass your user fixture directly
const testUser: User = { id: 1, username: 'tester1', password: 'securePassword123' };
await login(testUser);
This makes your function's dependencies crystal clear and the code inside cleaner.
Optional and Default Parameters
Not all parameters are needed for every function call.
An optional parameter (marked with
?
) may or may not be passed.A default parameter is assigned a value if one isn't provided. Let's create a
registerUser
function where subscribing to the newsletter is optional, and accepting terms is on by default.
async function registerUser(
email: string,
subscribeToNewsletter?: boolean, // Optional parameter
acceptTerms: boolean = true // Default parameter
) {
// ... fill email field
// ... check the acceptTerms box if true
// We'll add logic for the optional param later!
}
This makes your function incredibly flexible, reducing the need for multiple function variations.
🔁 Mastering Loops: Automating Repetitive Actions
Tests often involve repeating the same check or action on multiple elements. Loops are your tool for automation at scale.
The for
Loop
The classic for
loop is great when you need to iterate a specific number of times, using an index.
Use Case: Verify that every product in a search result list is visible.
const productLocators = await page.locator('.product-item').all();
// Loop from the first element (index 0) to the last
for (let i = 0; i < productLocators.length; i++) {
console.log(`Checking product #${i + 1}`);
await expect(productLocators[i]).toBeVisible();
}
The for...of
Loop
A more modern and readable choice for iterating over array values. It does the same thing as the for
loop above, but with cleaner syntax.
const productLocators = await page.locator('.product-item').all();
// Loop through each locator directly, no index needed
for (const locator of productLocators) {
await expect(locator).toBeVisible();
}
Use for...of
whenever you need to work with each item in an array.
The for...in Loop
This loop is different: it iterates over the keys (properties) of an object.
Use Case: Logging all the settings from a configuration object.
const testConfig = {
browser: 'chromium',
headless: true,
timeout: 30000
};
// Loop through the keys: 'browser', 'headless', 'timeout'
for (const key in testConfig) {
console.log(`Config key: ${key}, Value: ${testConfig[key]}`);
}
The while
Loop
A while
loop runs as long as a certain condition is true
. This is perfect for when you don't know how many iterations you'll need.
Use Case: After submitting a form, wait for a "Success!" message to appear.
await page.getByRole('button', { name: 'Submit' }).click();
let successMessageVisible = false;
let attempts = 0;
// Keep checking as long as the message is NOT visible and we haven't timed out
while (!successMessageVisible && attempts < 10) {
successMessageVisible = await page.getByText('Success!').isVisible();
attempts++;
await page.waitForTimeout(500); // Wait half a second between checks
}
expect(successMessageVisible).toBe(true);
🧭 Conditional Logic: Making Your Tests "Think"
Conditional logic (if
, switch
) allows your tests to make decisions and react to different situations, making them far more robust.
if / else
Statements
This is the most common form of decision-making. "If this condition is true, do this. Otherwise, do that."
Let's revisit our registerUser
function and handle the optional subscribeToNewsletter
parameter.
async function registerUser(
email: string,
subscribeToNewsletter?: boolean,
acceptTerms: boolean = true
) {
await page.getByLabel('Email').fill(email);
// If the subscribeToNewsletter parameter was passed as true...
if (subscribeToNewsletter) {
// ...then click the newsletter checkbox.
await page.getByLabel('Subscribe to newsletter').check();
}
if (acceptTerms) {
await page.getByLabel('I accept the terms').check();
}
await page.getByRole('button', { name: 'Register' }).click();
}
Now our function intelligently handles whether to subscribe the user or not!
The Ternary Operator: A Shortcut for if/else
For simple conditions where you need to assign one of two values to a variable, a full if/else
block can be wordy. The ternary operator (? :
) is a clean, one-line alternative.
The syntax is condition ? value_if_true : value_if_false
.
Use Case: Construct a full URL, but only add a base URL if it's actually provided.
async function createFullURL(path: string, baseURL?: string) {
// If baseURL exists, combine them. Otherwise, just use path.
const fullURL = baseURL ? `${baseURL}${path}` : path;
return fullURL;
}
// Example calls:
// Returns '/api/users'
const url1 = await createFullURL('/api/users');
// Returns 'https://my-api.com/api/users'
const url2 = await createFullURL('/api/users', 'https://my-api.com');
The switch
Statement
A switch
statement is a clean way to handle multiple distinct conditions based on a single value. It's often more readable than a long chain of if/else if
statements.
Use Case: Create a single function to handle different API request methods.
type ApiMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
async function sendApiRequest(method: ApiMethod, url: string) {
switch (method) {
case 'GET':
return await api.get(url);
case 'POST':
return await api.post(url, { data: { name: 'New Item' } });
case 'PUT':
return await api.put(url, { data: { name: 'Updated Item' } });
case 'DELETE':
return await api.delete(url);
default:
// It's good practice to handle unexpected cases
throw new Error(`Unsupported API method: ${method}`);
}
}
// Easy to call with the desired method
const response = await sendApiRequest('GET', '/api/items');
🚀 Your Next Step: From Scripts to a Framework
You've just learned the building blocks of programming logic. Functions, Loops, and Conditionals are what elevate a simple script into a robust, maintainable, and intelligent automation framework. Stop copying and pasting code. Start building reusable, thinking machines.
Your Mission:
Find a test in your project where you repeat similar actions. For example, logging in with 3 different user types.
Refactor it!
Create a single, reusable
login
function that accepts a user object.Create an array of user objects (
const users = [...]
).Use a
for...of
loop to iterate through your array and call yourlogin
function for each user.Bonus: Add a conditional inside the loop. For example,
if (user.role === 'admin')
, perform an extra check to verify an admin-only element is visible.
Take on the challenge. You'll see immediately how much cleaner and more powerful your tests become.
Subscribe to my newsletter
Read articles from Ivan Davidov directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Ivan Davidov
Ivan Davidov
Automation QA Engineer, ISTQB CTFL, PSM I, helping teams improve the quality of the product they deliver to their customers. • Led the development of end-to-end (E2E) and API testing frameworks from scratch using Playwright and TypeScript, ensuring robust and scalable test automation solutions. • Integrated automated tests into CI/CD pipelines to enhance continuous integration and delivery. • Created comprehensive test strategies and plans to improve test coverage and effectiveness. • Designed performance testing frameworks using k6 to optimize system scalability and reliability. • Provided accurate project estimations for QA activities, aiding effective project planning. • Worked with development and product teams to align testing efforts with business and technical requirements. • Improved QA processes, tools, and methodologies for increased testing efficiency. • Domain experience: banking, pharmaceutical and civil engineering. Bringing over 3 year of experience in Software Engineering, 7 years of experience in Civil engineering project management, team leadership and project design, to the table, I champion a disciplined, results-driven approach, boasting a record of more than 35 successful projects.