QA Interview Hub · Knowledge base · Quizzes

TypeScript

Utility types

Partial, Required, Readonly, Pick, Omit

Utility types are built-in generic types that transform other types. They let you avoid duplicating models.

UtilityWhat it does
Partial<T>all fields become optional
Required<T>all fields become required
Readonly<T>all fields readonly (shallowly)
Pick<T, K>keeps only the listed keys
Omit<T, K>removes the listed keys

A typical use in automation is a test data factory. The base object is valid, and the test overrides only what matters for the scenario:

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 is handy for a creation body: the server generates id and createdAt, so the request is the model without them:

interface Account { id: string; iban: string; currency: string; createdAt: string }
type CreateAccountBody = Omit<Account, 'id' | 'createdAt'>;
type AccountSummary = Pick<Account, 'id' | 'iban'>;

A nuance: Omit doesn't check that the key exists. Omit<Account, 'idd'> compiles without errors. Pick does check keys.

In an interview: I use Partial in test data factories, so a test overrides only the needed fields, and Omit and Pick to derive a request type from a model without duplication. That way, when the model changes, all derived types update automatically.

Record, Exclude, Extract, NonNullable

Record<K, V> is an object with keys K and values V. If K is a union of literals, TS requires all the keys. This is convenient for lookup tables that must be complete:

type Currency = 'UAH' | 'USD' | 'EUR';

const minorUnits: Record<Currency, number> = {
  UAH: 2,
  USD: 2,
  EUR: 2,
};
// add 'JPY' to Currency -> an error until you add the key

Exclude<T, U> removes from a union the variants that match U. Extract<T, U> does the opposite, keeping only them. NonNullable<T> removes null and 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

A practical example: a helper that waits for a transaction's final status accepts only FinalStatus. Passing 'PENDING' as the expected final state won't work, and a meaningless test won't compile.

A trap with Record<string, V>: access map['anyKey'] has the type V, though the key may be absent. Here noUncheckedIndexedAccess or the type Partial<Record<string, V>> helps.

In an interview: Record over a union of keys guarantees that a map covers all the variants, for example all currencies. Exclude and Extract filter a union, and that is how I pick out, for example, only the final transaction statuses for a wait helper.

ReturnType, Parameters, Awaited

These utilities derive types from functions. They are useful when a model isn't described explicitly or when you don't want to duplicate it.

  • ReturnType<typeof fn> is the type a function returns.
  • Parameters<typeof fn> is a tuple of parameter types.
  • Awaited<T> unwraps a Promise: Awaited<Promise<Account>> gives Account. It also works with nested promises.
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]

Where I use this:

  • The result type of a factory or a fixture without a separate interface.
  • A wrapper over a function that keeps its signature, for example step logging:
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>>;
  };
}

The any[] in the constraint here is a deliberate and accepted technique: it is needed so that any function fits the constraint.

A limitation: if the source of truth is an external contract (OpenAPI), it is better to generate types from it than to derive them from the implementation. Otherwise the type adjusts to a bug in the function.

In an interview: ReturnType and Parameters derive types from a function, and Awaited unwraps a promise, so Awaited<ReturnType<typeof fn>> gives the result type of an async function. I use this for factories and wrappers, but I take API contracts from OpenAPI, not from the implementation.