Utility Types
The built-in utility types and when to use them: Partial, Required, Readonly, Pick, Omit, Record, Exclude, Extract, NonNullable, ReturnType, Parameters and Awaited.
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,RequiredandReadonlyaffect only the top level. There is no built-inDeepPartialorDeepReadonly; 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.