QA Interview Hub · База знань · Тести
TypeScript
Generics
Навіщо generics: типізований API-клієнт
Generic - це параметр типу. Функція або клас працюють з різними типами, але зберігають зв'язок між входом і виходом. Без generics доводиться обирати між дублюванням коду і any.
class ApiClient {
constructor(private readonly baseUrl: string) {}
async get<T>(path: string): Promise<T> {
const res = await fetch(`${this.baseUrl}${path}`);
if (!res.ok) throw new Error(`GET ${path} -> ${res.status}`);
return (await res.json()) as T;
}
}
const account = await client.get<Account>('/api/v1/accounts/42');
account.balance; // number, є автодоповнення
Тут важливо бути чесним: as T - це твердження, а не перевірка. Якщо сервер повернув інший об'єкт, тип збреше. Тому в зрілому фреймворку generic приймає схему, а тип виводиться з неї:
import { z } from 'zod';
async function getParsed<S extends z.ZodTypeAny>(path: string, schema: S): Promise<z.infer<S>> {
const res = await fetch(path);
return schema.parse(await res.json());
}
Тепер T не передається вручну, а виводиться з аргументу. Це загальне правило: найкращі generics ті, які TS виводить сам із параметрів, а не ті, які треба вказувати в кутових дужках.
Інші типові місця generics у тестах: ApiResult<T>, пагінована відповідь Page<T>, хелпер retry<T>(fn: () => Promise<T>): Promise<T>, фабрика тестових даних build<T>(defaults: T, overrides?: Partial<T>): T.
На співбесіді: Generics дозволяють написати один метод get<T>, який повертає правильний тип для будь-якого ендпоінта. Але as T нічого не перевіряє, тому я передаю в клієнт схему, а тип виводиться з неї - так тип і реальні дані не розходяться.
Обмеження, keyof і значення за замовчуванням
Обмеження extends каже, що параметр типу має відповідати певній формі. Без нього всередині функції про T нічого не відомо.
interface HasId { id: string }
function findById<T extends HasId>(items: readonly T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
Комбінація з keyof дає типобезпечний доступ до полів. Класичний приклад - хелпер, який дістає колонку з результатів:
function pluck<T, K extends keyof T>(rows: readonly T[], key: K): T[K][] {
return rows.map(r => r[key]);
}
const amounts = pluck(transactions, 'amount'); // number[]
// pluck(transactions, 'amout'); // помилка: опечатка в назві поля
T[K] - indexed access type, тип поля K в об'єкті T. Повернений тип точно відповідає вибраному полю.
Параметр типу може мати значення за замовчуванням:
interface Page<T, Meta = { total: number }> {
items: T[];
meta: Meta;
}
type AccountsPage = Page<Account>;
Типові помилки з generics:
- Параметр типу використовується лише один раз.
function log<T>(x: T): void нічим не краща за x: unknown. Generic має сенс, коли пов'язує кілька місць. - Ручне вказання типу там, де його можна вивести.
- Надто складні generics у тестовому коді. Тест повинен читатися швидко, складна типізація - в бібліотечному шарі фреймворку.
На співбесіді: Через extends я обмежую параметр типу, наприклад T extends HasId, а K extends keyof T дає безпечний доступ до полів без опечаток. Якщо параметр типу з'являється тільки один раз, generic не потрібен.
Generics у Playwright: фікстури
У Playwright generics найчастіше зустрічаються у фікстурах. test.extend<T>() приймає тип, який описує нові фікстури, і після цього вони типізовані в кожному тесті.
import { test as base, expect } from '@playwright/test';
import { LoginPage } from './pages/login-page';
import { ApiClient } from './api/api-client';
type Fixtures = {
loginPage: LoginPage;
api: ApiClient;
};
export const test = base.extend<Fixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
api: async ({ request }, use) => {
await use(new ApiClient(request));
},
});
test('user sees balance', async ({ loginPage, api }) => {
// loginPage: LoginPage, api: ApiClient
});
Для worker-scoped фікстур є другий параметр: base.extend<TestFixtures, WorkerFixtures>(). Наприклад, тестовий акаунт, який створюється один раз на воркер.
Ще одне місце - типізація опцій проєкту. Фікстуру можна оголосити як опцію через { option: true } і задавати її значення в playwright.config.ts для різних проєктів, наприклад різні валюти або ринки.
Generics також корисні в хелперах очікування:
async function pollUntil<T>(fn: () => Promise<T>, done: (v: T) => boolean, timeoutMs = 10_000): Promise<T> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const value = await fn();
if (done(value)) return value;
await new Promise(r => setTimeout(r, 500));
}
throw new Error('pollUntil timeout');
}
Хоча в Playwright для цього вже є expect.poll, і я використовую його в першу чергу.
На співбесіді: У Playwright я типізую фікстури через test.extend<Fixtures>(), і тоді Page Object та API-клієнт мають тип у кожному тесті. Для фікстур рівня воркера є другий параметр типу.