Advanced Framework Typing
Welcome to the advanced class! When building a massive enterprise Playwright framework, you want to write code *once* and reuse it everywhere. These advanced TypeScript features allow us to build highly flexible, reusable helper functions that automatically adapt to whatever data you throw at them. Let's dive in!
1. Basic Generics
Think of Generics () as a "placeholder variable for a Type". Instead of writing a helper function that only works for Users, you use to create a helper that works for *anything*.
typescript
// The acts as a placeholder. Whatever type we pass in, it returns a Promise of that type.
async function fetchApiData(endpoint: string): Promise {
const response = await page.request.get(endpoint);
return response.json() as Promise;
}
// We pass the type, so TS knows 'userData' has an id and name!
const userData = await fetchApiData('/api/users/1');
2. Utility Types
Sometimes you have a massive interface (like a User with 50 fields), but for a specific test, you only want to update *one* field. Utility types like Partial let you make all fields optional instantly!
typescript
interface UserProfile { id: string; name: string; email: string; role: string; }
// Partial makes every field optional, so we don't have to provide all 4 fields just to update the email!
type UpdateUserPayload = Partial;
const updateData: UpdateUserPayload = { email: 'new@careerraah.com' }; // Perfectly valid!
3. Keyof
The keyof operator grabs all the keys of an object and turns them into a strict union type. This prevents typos when a function expects a property name.
typescript
interface Car { brand: string; year: number; }
// You can ONLY pass 'brand' or 'year' into this function.
function getCarProperty(property: keyof Car) {
console.log(Fetching ${property}...);
}
getCarProperty('brand'); // Works!
// getCarProperty('color'); // TS Error: Argument of type '"color"' is not assignable.
4. Type Guards
When an API returns mixed data (e.g., an array containing both strings and numbers), Type Guards help us safely separate them before acting on them.
typescript
const responseData: (string | number)[] = ['success', 404, 'pending'];
for (const item of responseData) {
if (typeof item === 'number') {
// TS now guarantees 'item' is a number here, so we can do math!
console.log(item + 10);
} else {
// TS knows it must be a string here.
console.log(item.toUpperCase());
}
}
5. Conditional Types
This is like writing an if/else statement, but for *Types* instead of values! It allows our framework to dynamically choose a return type based on what we input.
typescript
// If T is a string, return a boolean. Otherwise, return a number.
type IsString = T extends string ? boolean : number;
let result1: IsString<"Hello">; // Type becomes 'boolean'
let result2: IsString<42>; // Type becomes 'number'
6. Mapped Types
Mapped types act like a for...loop to iterate over an existing type and modify all its properties at once.
typescript
interface Settings { theme: string; notifications: boolean; }
// Loops over the keys of Settings and changes their values to boolean
type ToggleSettings = {
[Key in keyof Settings]: boolean;
};
// Now 'theme' and 'notifications' both MUST be booleans!
const userToggles: ToggleSettings = { theme: true, notifications: false };
7. Index Signatures
Sometimes an API returns a massive JSON object where you *don't know* what the keys will be in advance. Index signatures let you say, "This object will have string keys, and any kind of value."
typescript
// We don't know the keys, but we know they are strings, and the values are strings.
interface DynamicHeaders {
[key: string]: string;
}
const headers: DynamicHeaders = {
"Authorization": "Bearer token",
"X-Custom-Header": "careerraah-123"
};
8. Declaration Merging
If you declare an interface twice, TypeScript automatically merges them together! We use this in Playwright to easily add our own custom fixtures to Playwright's default TestFixtures object.
typescript
// First declaration
interface Box { height: number; }
// Second declaration somewhere else
interface Box { width: number; }
// TypeScript merged them!
const myBox: Box = { height: 10, width: 20 };
9. Decorators
Decorators (@) are a meta-programming feature that let you wrap a function to modify its behavior without changing its code. They are incredibly useful for adding automatic logging or retries to Page Object methods!
typescript
// A decorator that automatically logs whenever a method is called
function LogClick(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log([UI Action]: Clicking element...);
return originalMethod.apply(this, args);
};
}
class LoginPage {
@LogClick
async submitBtn() {
await page.locator('#login').click();
}
}