level 01 / generics-and-utility-types

Generics & Utility Types

Generic functions and TypeScript's built-in utility types — Partial, Required, Pick, Record, Readonly — used throughout Playwright fixtures and test data builders.

Generics

Generics let you write functions and types that work with many types while preserving type information:

// Without generics — loses type information
function first(arr: any[]): any {
  return arr[0];
}

// With generics — preserves type
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const names = ['Alice', 'Bob', 'Carol'];
const name = first(names); // inferred: string | undefined

const counts = [3, 1, 4, 1, 5];
const count = first(counts); // inferred: number | undefined

Generic constraints

// T must have a name property
function printName<T extends { name: string }>(item: T): void {
  console.log(item.name);
}

// T must be a key of the object
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, username: 'alice', email: 'alice@test.com' };
const username = getProperty(user, 'username'); // string
const id = getProperty(user, 'id');             // number
// getProperty(user, 'missing');                 // Error at compile time

Utility types

TypeScript ships with utility types that transform existing types. These appear throughout Playwright fixture and configuration types.

Partial — make all properties optional

interface LoginCredentials {
  username: string;
  password: string;
  twoFactorCode: string;
}

// For test data builders — not all fields required at construction time
type PartialCredentials = Partial<LoginCredentials>;
// { username?: string; password?: string; twoFactorCode?: string }

function buildCredentials(overrides: Partial<LoginCredentials> = {}): LoginCredentials {
  return {
    username: 'standard_user',
    password: 'secret_sauce',
    twoFactorCode: '000000',
    ...overrides,
  };
}

Required — make all properties required

interface TestConfig {
  baseURL?: string;
  timeout?: number;
}

type RequiredConfig = Required<TestConfig>;
// { baseURL: string; timeout: number }

Pick — select specific properties

interface User {
  id: number;
  username: string;
  email: string;
  role: string;
  createdAt: Date;
}

// Only need these fields for login tests
type LoginUser = Pick<User, 'username' | 'email'>;
// { username: string; email: string }

Record — typed key-value map

// Map browser names to their expected headings
type BrowserName = 'chromium' | 'firefox' | 'webkit';
const browserVersions: Record<BrowserName, string> = {
  chromium: '120.0',
  firefox: '121.0',
  webkit: '17.4',
};

// Test data indexed by test ID
const testData: Record<string, { username: string; expected: string }> = {
  'test-001': { username: 'standard_user', expected: 'Products' },
  'test-002': { username: 'locked_out_user', expected: 'Epic sadface' },
};

Readonly — prevent mutation

// Playwright's Page is effectively readonly — you don't replace it
const config: Readonly<{ baseURL: string; timeout: number }> = {
  baseURL: 'https://saucedemo.com',
  timeout: 30_000,
};
// config.timeout = 5_000; // Error: Cannot assign to readonly property

Try it: generics and utility types

Interview questions