# 🔀 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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1749751326101/d24753d0-e25a-4f06-8260-ccbbf298ea65.jpeg align="center")

---

### ✅ Prerequisites

This article assumes you are comfortable with the topics from our previous discussion:

* TypeScript Fundamentals:
    
    * [Basic TypeScript types (`string`, `number`, `boolean`)](https://idavidov.eu/your-first-steps-in-typescript-a-practical-roadmap-for-automation-qa)
        
    * [Structuring data with `Arrays` and `Objects`](https://idavidov.eu/how-to-use-arrays-and-objects-in-typescript-for-powerful-qa-automation-scripts) 

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.

```typescript
// 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.

```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.

```typescript
// 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.
    

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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);
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1749751236820/b350a044-e732-443c-b2c9-8f53929b897d.jpeg align="center")

---

### 🧭 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.

```typescript
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.

```ts
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.

```ts
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');
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1749751397841/b053c38d-b22e-445d-839c-6b70542747b8.jpeg align="center")

---

### 🚀 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!

1. Create a single, reusable `login` function that accepts a user object.
    
2. Create an array of user objects (`const users = [...]`).
    
3. Use a `for...of` loop to iterate through your array and call your `login` function for each user.
    
4. **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.

---

> **🙏🏻 Thank you for reading!** Building robust, scalable automation frameworks is a journey best taken together. If you found this article helpful, consider joining a growing community of QA professionals 🚀 who are passionate about mastering modern testing.

> [Join the community and get the latest articles and tips by signing up for the newsletter.](https://idavidov.eu/newsletter)
