What utility types are

A utility type is one of the generic types that TypeScript provides globally, without an import, to transform types: make properties optional, select some of them, remove members from a union, or read the return type of a function. They are ordinary generic types, declared in the standard library files that come with the compiler, and built from the type operators and the mapped and conditional types described elsewhere. You use them like any generic type, by passing type arguments in angle brackets.

Their purpose is to avoid writing the same shape twice. Instead of a second interface TicketUpdate that repeats every field of Ticket with a ?, you write Partial<Ticket>, and when a field is added to Ticket, the update type follows.

Utility type Result
Partial<T> all properties of T optional
Required<T> all properties required
Readonly<T> all properties readonly (one level deep)
Pick<T, K> only the properties named in K
Omit<T, K> all properties except those named in K
Record<K, T> an object with keys K and values T
Exclude<U, E> the members of union U not assignable to E
Extract<U, E> the members of union U assignable to E
NonNullable<T> T without null and undefined
ReturnType<F> the return type of function type F
Parameters<F> the parameter types of F, as a tuple
Awaited<T> the value a promise type resolves to

There are a few more, such as InstanceType, ConstructorParameters, NoInfer and the string helpers Uppercase, Lowercase, Capitalize and Uncapitalize; the twelve above cover most everyday code.

Partial, Required and Readonly

Partial<T> is the type T with every property made optional. Its classic use is an update: the caller passes only the fields that change, and the function merges them into the existing object with spread syntax.

interface Tariff {
  code: "DAY" | "NIGHT";
  pricePerKWh: number;
  validFrom: string;
}

function updateTariff(current: Tariff, changes: Partial<Tariff>): Tariff {
  return { ...current, ...changes };
}

const day: Tariff = { code: "DAY", pricePerKWh: 0.32, validFrom: "2026-01-01" };
const raised = updateTariff(day, { pricePerKWh: 0.34, validFrom: "2026-11-01" });
console.log(raised);
console.log(day.pricePerKWh);       // the original is unchanged
{ code: 'DAY', pricePerKWh: 0.34, validFrom: '2026-11-01' }
0.32

Every property is optional, but the names are still checked, so a misspelled field is not silently ignored:

interface Tariff {
  code: "DAY" | "NIGHT";
  pricePerKWh: number;
}

function updateTariff(current: Tariff, changes: Partial<Tariff>): Tariff {
  return { ...current, ...changes };
}

const day: Tariff = { code: "DAY", pricePerKWh: 0.32 };
updateTariff(day, { pricePerKwh: 0.34 });
example.ts(11,21): error TS2561: Object literal may only specify known properties, but 'pricePerKwh' does not exist in type 'Partial<Tariff>'. Did you mean to write 'pricePerKWh'?

Required<T> is the opposite: it removes every ?. It suits settings that are optional for the caller but complete after defaults have been applied:

interface Settings {
  region?: string;
  vatRate?: number;
}

function describe(s: Required<Settings>): string {
  return `${s.region} ${(s.vatRate * 100).toFixed(0)}%`;   // no undefined checks needed
}

const defaults: Required<Settings> = { region: "NL", vatRate: 0.21 };
console.log(describe({ ...defaults, region: "BE" }));
describe({ region: "DE" });
example.ts(12,10): error TS2741: Property 'vatRate' is missing in type '{ region: string; }' but required in type 'Required<Settings>'.

Readonly<T> marks every property readonly, which is a useful way to say "this function will not change your object". Two limits: it is shallow, so nested objects can still be changed, and like all types it is gone at run time (it does not call Object.freeze):

interface Customer {
  id: number;
  name: string;
  address: { city: string };
}

const c: Readonly<Customer> = { id: 4471, name: "Amara Okafor", address: { city: "Utrecht" } };
c.address.city = "Delft";          // allowed: Readonly is shallow
c.name = "A. Okafor";
example.ts(9,3): error TS2540: Cannot assign to 'name' because it is a read-only property.

The same distinction between a shallow and a deep copy is explained for objects in Copying objects. ReadonlyArray<T>, written readonly T[], is the array version: it has no push, pop or sort.

Pick and Omit

Pick<T, K> builds a type from only the properties of T named in K, a union of property names. Omit<T, K> does the reverse: it keeps every property except the ones named in K. Use Pick when you want a few fields, Omit when you want all but a few.

interface Ticket {
  id: string;
  title: string;
  homes: number;
  internalNotes: string;
  assignee: string;
}

type TicketSummary = Pick<Ticket, "id" | "title">;
type PublicTicket = Omit<Ticket, "internalNotes" | "assignee">;

const ticket: Ticket = {
  id: "INC-10234", title: "No power in Utrecht", homes: 120,
  internalNotes: "Cable damage near substation", assignee: "Tomasz Nowak",
};

function toPublic({ internalNotes, assignee, ...rest }: Ticket): PublicTicket {
  return rest;
}

const summary: TicketSummary = { id: ticket.id, title: ticket.title };
console.log(summary);
console.log(toPublic(ticket));
{ id: 'INC-10234', title: 'No power in Utrecht' }
{ id: 'INC-10234', title: 'No power in Utrecht', homes: 120 }

The two differ in one important detail. Pick requires its keys to exist (K extends keyof T), but Omit accepts any key, so a typo in an Omit removes nothing and reports nothing:

interface Ticket {
  id: string;
  title: string;
  internalNotes: string;
}

type PublicTicket = Omit<Ticket, "internalNote">;    // typo: no error

const p: PublicTicket = { id: "INC-10234", title: "No power", internalNotes: "…" };
console.log(Object.keys(p));
[ 'id', 'title', 'internalNotes' ]
interface Ticket {
  id: string;
  title: string;
  internalNotes: string;
}

type Notes = Pick<Ticket, "internalNote">;
example.ts(7,27): error TS2344: Type '"internalNote"' does not satisfy the constraint 'keyof Ticket'.

Omit does not remove data

Omit changes a type, never an object. Because TypeScript types are structural, a full Ticket is perfectly acceptable where a PublicTicket is expected: it has every property the type asks for, and extra properties are allowed. The internal notes are still in the object, and JSON.stringify sends them along:

interface Ticket {
  id: string;
  title: string;
  internalNotes: string;
}

type PublicTicket = Omit<Ticket, "internalNotes">;

const ticket: Ticket = {
  id: "INC-10234",
  title: "No power in Utrecht",
  internalNotes: "Cable damage, contractor at fault",
};

const forCustomer: PublicTicket = ticket;          // compiles: extra properties are fine
console.log(JSON.stringify(forCustomer));

function toPublic({ internalNotes, ...rest }: Ticket): PublicTicket {
  return rest;                                     // this really leaves it out
}
console.log(JSON.stringify(toPublic(ticket)));
{"id":"INC-10234","title":"No power in Utrecht","internalNotes":"Cable damage, contractor at fault"}
{"id":"INC-10234","title":"No power in Utrecht"}

Record

Record<K, T> is an object type whose keys are K and whose values all have type T. It behaves very differently depending on K. With a union of literal keys, every key is required, so the compiler checks that a lookup table is complete:

type TariffCode = "DAY" | "NIGHT" | "PEAK";

const prices: Record<TariffCode, number> = { DAY: 0.32, NIGHT: 0.25 };
example.ts(3,7): error TS2741: Property 'PEAK' is missing in type '{ DAY: number; NIGHT: number; }' but required in type 'Record<TariffCode, number>'.

Add a fourth tariff code to the union later, and every Record<TariffCode, …> in the program reports the missing entry. With string as the key type, on the other hand, a Record is a dictionary with any keys, and the compiler cannot know which ones exist:

type TariffCode = "DAY" | "NIGHT" | "PEAK";

const prices: Record<TariffCode, number> = { DAY: 0.32, NIGHT: 0.25, PEAK: 0.41 };
const homesPerRegion: Record<string, number> = { Utrecht: 120, Delft: 40 };

for (const code of ["DAY", "PEAK"] as const) {
  console.log(code, prices[code].toFixed(2));
}
console.log(homesPerRegion["Utrecht"], homesPerRegion["Assen"]);   // type says number
DAY 0.32
PEAK 0.41
120 undefined

The type of homesPerRegion["Assen"] is number, yet the value is undefined. The compiler option noUncheckedIndexedAccess adds | undefined to such lookups; see null, undefined and Strict Null Checks. For a dictionary that changes at run time, a Map is often the better choice: its get method is typed as returning V | undefined from the start.

Exclude, Extract and NonNullable

Three utilities work on union types instead of objects. Exclude<U, E> removes the members of U that are assignable to E; Extract<U, E> keeps only those; NonNullable<T> removes null and undefined.

type Status = "open" | "in-progress" | "resolved" | "closed";

type ActiveStatus = Exclude<Status, "resolved" | "closed">;   // "open" | "in-progress"
type DoneStatus = Extract<Status, "resolved" | "closed" | "canceled">;
type City = NonNullable<string | null | undefined>;           // string

const active: ActiveStatus[] = ["open", "in-progress"];
const done: DoneStatus = "closed";
const city: City = "Utrecht";
console.log(active, done, city);
[ 'open', 'in-progress' ] closed Utrecht

"canceled" is not a member of Status, so Extract simply ignores it: DoneStatus is "resolved" | "closed". The derived types are checked like any other:

type Status = "open" | "in-progress" | "resolved" | "closed";
type ActiveStatus = Exclude<Status, "resolved" | "closed">;

const next: ActiveStatus = "closed";
example.ts(4,7): error TS2322: Type '"closed"' is not assignable to type 'ActiveStatus'.
const city: NonNullable<string | null | undefined> = null;
example.ts(1,7): error TS2322: Type 'null' is not assignable to type 'string'.

Exclude is not the same as Omit: Exclude removes members from a union of types, Omit removes properties from an object type.

ReturnType, Parameters and Awaited

ReturnType<T> gives the return type of a function type T. Since it needs a type, you pass it the type of a function with a type query: ReturnType<typeof createTicket>. Parameters<T> gives the parameter types as a tuple, and Awaited<T> unwraps a Promise, including nested ones, to the type of the value it resolves to.

function createTicket(region: string, homes: number, priority: 1 | 2 | 3 = 3) {
  return { id: `INC-${10230 + homes}`, region, homes, priority };
}

type NewTicket = ReturnType<typeof createTicket>;
type CreateArgs = Parameters<typeof createTicket>;   // [region: string, homes: number, priority?: 1 | 2 | 3]

const args: CreateArgs = ["Delft", 40];
const t: NewTicket = createTicket(...args);
console.log(t);

async function loadTicket(id: string) {
  return { id, title: "No power in Utrecht", homes: 120 };
}

type Loaded = Awaited<ReturnType<typeof loadTicket>>;

async function main() {
  const loaded: Loaded = await loadTicket("INC-10234");
  console.log(loaded.title);
}
main();
{ id: 'INC-10270', region: 'Delft', homes: 40, priority: 3 }
No power in Utrecht

These are most useful for functions you do not control, such as a library function whose result type is not exported, and for factory functions whose result is the definition of a type. For an async function, forgetting Awaited gives you the promise type instead of the data:

async function loadTicket(id: string) {
  return { id, homes: 120 };
}

type Wrong = ReturnType<typeof loadTicket>;          // Promise<{ id: string; homes: number }>
const t: Wrong = { id: "INC-10234", homes: 120 };
example.ts(6,20): error TS2353: Object literal may only specify known properties, and 'id' does not exist in type 'Promise<{ id: string; homes: number; }>'.

Typing promises and async functions in general is covered in Async Code and Typing API Data.

Combining utility types

Utility types nest, and an intersection (&) joins their results. A common case is an update that must name the record it changes but may change any other field: the id is required, everything else optional.

interface Ticket {
  id: string;
  title: string;
  homes: number;
  status: "open" | "resolved";
}

type TicketPatch = Pick<Ticket, "id"> & Partial<Omit<Ticket, "id">>;

const tickets = new Map<string, Ticket>([
  ["INC-10234", { id: "INC-10234", title: "No power in Utrecht", homes: 120, status: "open" }],
]);

function patch(change: TicketPatch): Ticket | undefined {
  const current = tickets.get(change.id);
  if (current === undefined) return undefined;
  const updated = { ...current, ...change };
  tickets.set(change.id, updated);
  return updated;
}

console.log(patch({ id: "INC-10234", status: "resolved" }));
console.log(patch({ id: "INC-99999", homes: 3 }));
{
  id: 'INC-10234',
  title: 'No power in Utrecht',
  homes: 120,
  status: 'resolved'
}
undefined
interface Ticket {
  id: string;
  title: string;
  status: "open" | "resolved";
}

type TicketPatch = Pick<Ticket, "id"> & Partial<Omit<Ticket, "id">>;

const p: TicketPatch = { status: "resolved" };
example.ts(9,7): error TS2322: Type '{ status: "resolved"; }' is not assignable to type 'TicketPatch'.

Give such combinations a name with a type alias, as above. A well-named TicketPatch is easier to read in an error message, and in an editor tooltip, than the expression it stands for.

How they are built, and their limits

Utility types are not magic: they are a few lines each in the standard library that ships with TypeScript (the file lib.es5.d.ts). These are the actual definitions of four of them:

type Partial<T> = {
    [P in keyof T]?: T[P];
};

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

type Exclude<T, U> = T extends U ? never : T;

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Partial and Pick are mapped types that loop over keys with keyof and read property types with indexed access; Exclude is a conditional type. The definition of Omit also explains its typo blindness: K extends keyof any allows every possible key, not just the keys of T.

Keep their limits in mind:

  • Compile time only. No utility type validates, copies, freezes or strips data. Data from outside still needs a run-time check; see Async Code and Typing API Data.
  • Shallow. Partial, Required and Readonly affect only the top level. There is no built-in DeepPartial or DeepReadonly; you can write one with a recursive mapped type, but deep transformations of large types make errors hard to read.
  • Readable names help. Hovering over a type in an editor shows the computed result; a named alias keeps both the intent and the error messages clear.