QA Interview Hub · База знань · Тести
TypeScript
Utility types
Partial, Required, Readonly, Pick, Omit
Utility types - вбудовані generic-типи, які трансформують інші типи. Вони дозволяють не дублювати моделі.
| Утиліта | Що робить |
|---|
Partial<T> | усі поля стають optional |
Required<T> | усі поля стають обов'язковими |
Readonly<T> | усі поля readonly (поверхнево) |
Pick<T, K> | лишає тільки вказані ключі |
Omit<T, K> | прибирає вказані ключі |
Типове застосування в автоматизації - фабрика тестових даних. Базовий об'єкт валідний, тест перевизначає тільки те, що важливо для сценарію:
interface TransferRequest {
fromAccount: string;
toAccount: string;
amount: number;
currency: 'UAH' | 'USD';
description: string;
}
function buildTransfer(overrides: Partial<TransferRequest> = {}): TransferRequest {
return {
fromAccount: 'UA213223130000026007233566001',
toAccount: 'UA903052992990004149123456789',
amount: 10_000,
currency: 'UAH',
description: 'autotest',
...overrides,
};
}
const negative = buildTransfer({ amount: -1 });
Omit зручний для тіла створення: сервер генерує id і createdAt, тому запит - це модель без них:
interface Account { id: string; iban: string; currency: string; createdAt: string }
type CreateAccountBody = Omit<Account, 'id' | 'createdAt'>;
type AccountSummary = Pick<Account, 'id' | 'iban'>;
Нюанс: Omit не перевіряє, що ключ існує. Omit<Account, 'idd'> компілюється без помилок. Pick ключі перевіряє.
На співбесіді: Я використовую Partial у фабриках тестових даних, щоб тест перевизначав тільки потрібні поля, а Omit і Pick - щоб вивести тип запиту з моделі без дублювання. Так при зміні моделі всі похідні типи оновлюються автоматично.
Record, Exclude, Extract, NonNullable
Record<K, V> - об'єкт з ключами K і значеннями V. Якщо K - union літералів, TS вимагає всі ключі. Це зручно для таблиць відповідності, які мають бути повними:
type Currency = 'UAH' | 'USD' | 'EUR';
const minorUnits: Record<Currency, number> = {
UAH: 2,
USD: 2,
EUR: 2,
};
// додали 'JPY' в Currency -> помилка, поки не додасте ключ
Exclude<T, U> прибирає з union варіанти, які підходять під U. Extract<T, U> - навпаки, залишає тільки їх. NonNullable<T> прибирає null і undefined.
type TxStatus = 'PENDING' | 'COMPLETED' | 'FAILED' | 'REVERSED';
type FinalStatus = Exclude<TxStatus, 'PENDING'>;
// 'COMPLETED' | 'FAILED' | 'REVERSED'
type Event =
| { type: 'deposit'; amount: number }
| { type: 'withdrawal'; amount: number }
| { type: 'blocked'; reason: string };
type MoneyEvent = Extract<Event, { amount: number }>;
type MaybeId = string | null | undefined;
type Id = NonNullable<MaybeId>; // string
Практичний приклад: хелпер очікування фінального статусу транзакції приймає тільки FinalStatus. Передати 'PENDING' як очікуваний фінальний стан не вийде, і безглуздий тест не скомпілюється.
Пастка з Record<string, V>: доступ map['anyKey'] має тип V, хоча ключа може не бути. Тут допомагає noUncheckedIndexedAccess або тип Partial<Record<string, V>>.
На співбесіді: Record з union ключів гарантує, що мапа покриває всі варіанти, наприклад всі валюти. Exclude і Extract фільтрують union, і я так, наприклад, виділяю тільки фінальні статуси транзакції для хелпера очікування.
ReturnType, Parameters, Awaited
Ці утиліти виводять типи з функцій. Вони корисні, коли модель не описана явно або коли не хочеться її дублювати.
ReturnType<typeof fn> - тип, який повертає функція.Parameters<typeof fn> - кортеж типів параметрів.Awaited<T> - розгортає Promise: Awaited<Promise<Account>> дає Account. Працює і з вкладеними промісами.
async function createTestUser(role: 'admin' | 'viewer', balance = 0) {
return { id: crypto.randomUUID(), role, balance, token: 'jwt...' };
}
type TestUser = Awaited<ReturnType<typeof createTestUser>>;
// { id: string; role: 'admin' | 'viewer'; balance: number; token: string }
type CreateArgs = Parameters<typeof createTestUser>;
// [role: 'admin' | 'viewer', balance?: number]
Де я це застосовую:
- Тип результату фабрики або фікстури без окремого інтерфейсу.
- Обгортка над функцією зі збереженням сигнатури, наприклад логування кроку:
function withLog<F extends (...args: any[]) => Promise<unknown>>(name: string, fn: F) {
return async (...args: Parameters<F>): Promise<Awaited<ReturnType<F>>> => {
console.log(`[step] ${name}`, args);
return (await fn(...args)) as Awaited<ReturnType<F>>;
};
}
any[] в обмеженні тут свідомий і загальноприйнятий прийом: він потрібен, щоб під обмеження підходила будь-яка функція.
Обмеження: якщо джерело правди - зовнішній контракт (OpenAPI), краще генерувати типи з нього, а не виводити з реалізації. Інакше тип підлаштується під баг у функції.
На співбесіді: ReturnType і Parameters виводять типи з функції, а Awaited розгортає проміс, тому Awaited<ReturnType<typeof fn>> дає тип результату async-функції. Я використовую це для фабрик і обгорток, але контракти API беру з OpenAPI, а не з реалізації.