QA Interview Hub · База знань · Тести
TypeScript
Просунуті типи: keyof, typeof, mapped, conditional, template literal
keyof, typeof і indexed access
keyof T дає union ключів типу. typeof value у позиції типу бере тип зі значення. T[K] - тип поля за ключем. Разом вони дозволяють виводити типи з одного джерела правди.
const ROUTES = {
login: '/login',
accounts: '/accounts',
transfers: '/transfers/new',
} as const;
type RouteName = keyof typeof ROUTES; // 'login' | 'accounts' | 'transfers'
type RoutePath = (typeof ROUTES)[RouteName]; // '/login' | '/accounts' | '/transfers/new'
async function goTo(page: Page, name: RouteName) {
await page.goto(ROUTES[name]);
}
Тепер goTo(page, 'acounts') не скомпілюється, а новий маршрут автоматично доступний.
Indexed access працює і з масивами: (typeof CURRENCIES)[number] дає union елементів кортежу. І з вкладеними типами: Account['owner']['address'].
Різниця між двома typeof: у виразі JS typeof x === 'string' - це рантайм-оператор, який повертає рядок. У позиції типу type T = typeof x - це оператор TS, який бере статичний тип змінної. Синтаксис однаковий, сенс різний.
Корисний патерн для тестових даних:
const testUsers = {
admin: { email: 'admin@test.io', role: 'admin' },
operator: { email: 'op@test.io', role: 'operator' },
} as const;
type TestUserKey = keyof typeof testUsers; // 'admin' | 'operator'
type Role = (typeof testUsers)[TestUserKey]['role'];
Фікстура або хелпер приймає TestUserKey, і опечатки в ролях зникають.
На співбесіді: keyof typeof CONFIG дає union ключів константного об'єкта, а T[K] - тип значення. Я так типізую маршрути, ролі і тестових користувачів, щоб був один об'єкт як джерело правди і жодних опечаток у рядках.
Mapped types і conditional types
Mapped type проходить по ключах і будує новий тип. Саме так реалізовані Partial, Readonly, Record.
type Nullable<T> = { [K in keyof T]: T[K] | null };
// модифікатори: -? прибирає optional, -readonly прибирає readonly
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
З TS 4.1 можна перейменовувати ключі через as:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => Promise<T[K]>;
};
interface Balance { available: number; blocked: number }
type BalanceApi = Getters<Balance>;
// { getAvailable: () => Promise<number>; getBlocked: () => Promise<number> }
Conditional type - це "if" на рівні типів: T extends U ? X : Y. Разом з infer він дозволяє витягти частину типу.
type ItemOf<T> = T extends readonly (infer I)[] ? I : never;
type Tx = ItemOf<Transaction[]>; // Transaction
type UnwrapResult<T> = T extends { status: 'success'; data: infer D } ? D : never;
Важлива властивість - дистрибутивність. Якщо T - "голий" параметр типу і в нього передали union, умова застосовується до кожного члена окремо. Так працює Exclude<T, U> = T extends U ? never : T. Щоб вимкнути дистрибутивність, параметр загортають у кортеж: [T] extends [U].
Чесно про практику: у тестах я пишу такі типи рідко. Вони потрібні в шарі фреймворку - типізований API-клієнт, генератори фабрик. Якщо тип складно прочитати колезі, краще спростити.
На співбесіді: Mapped type проходить по ключах keyof T і будує новий тип, так влаштовані Partial і Readonly. Conditional type - це умова на рівні типів, а з infer можна витягти, наприклад, тип елемента масиву. У тестах я використовую це помірно, переважно в бібліотечному шарі.
Template literal types
Template literal types будують рядкові типи за шаблоном, як template strings, тільки на рівні типів. При підстановці union виходить декартовий добуток.
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Version = 'v1' | 'v2';
type ApiPath = `/api/${Version}/${string}`;
type RouteKey = `${Method} ${ApiPath}`;
const key: RouteKey = 'POST /api/v1/transfers'; // ок
// const bad: RouteKey = 'POST /transfers'; // помилка
${string} означає будь-який рядок у цій позиції, ${number} - рядок, що виглядає як число.
Вбудовані утиліти для рядкових типів: Uppercase, Lowercase, Capitalize, Uncapitalize.
Практичні приклади в автоматизації:
- Тестові ідентифікатори для хелпера над
getByTestId, щоб дотримуватись конвенції іменування (тип TestId нижче). - Ключі подій або метрик (тип
TransferEvent нижче). - Формат даних (тип
IsoDate нижче) - грубе обмеження, яке ловить очевидні помилки, але не валідує дату повністю. Рядок '1-2-3' теж пройде.
type TestId = `${'btn' | 'input' | 'row'}-${string}`;
type TransferEvent = `transfer.${'created' | 'completed' | 'failed'}`;
type IsoDate = `${number}-${number}-${number}`;
type Env = 'dev' | 'stage';
type BaseUrl = `https://${Env}.bank.example`;
function urlFor(env: Env): BaseUrl {
return `https://${env}.bank.example`;
}
Обмеження: template literal types перевіряють лише літерали, відомі на етапі компіляції. Рядок з .env або з відповіді API має тип string, і його все одно треба валідувати в рантаймі.
На співбесіді: Template literal types задають шаблон рядка на рівні типів, наприклад метод плюс шлях, що починається з /api/. Я використовую їх для конвенцій - ключі маршрутів, test id, URL середовищ - але розумію, що це тільки компіляція, а дані ззовні валідую окремо.