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, а не з реалізації.