TypeScript Utility Types: Pick, Omit, Partial, and Record

Published Updated

Utility types are the transformers TypeScript ships with: take a type you already declared, and produce a related one without retyping the fields. They matter less for cleverness than for drift, because a hand-copied shape stops matching the model the first time somebody renames a field.

You do not need the whole shelf. This guide covers the handful that show up constantly in application code, what each one really does, and the point where deriving a type stops being clearer than naming one.

What a Utility Type Is

Most utility types are nothing special to the compiler. They are ordinary generic types declared in the library files that ship with TypeScript, available globally without an import, though a few such as Uppercase and NoInfer are compiler intrinsics rather than declarations you could write yourself:

// roughly how Partial is declared in TypeScript's bundled lib files
type Partial<T> = {
  [Key in keyof T]?: T[Key];
};

That is a mapped type: walk every key of T and re-declare it with an optional marker. Knowing the shape is useful mainly because it explains the limits. It works one level deep, it produces a new type rather than modifying T, and it exists only at compile time like everything else in the type layer.

Every example below uses one small model:

type User = {
  id: string;
  email: string;
  displayName: string | null;
  roles: string[];
  createdAt: Date;
};

Picking and Omitting Fields

Pick keeps the fields you name, and Omit removes them from the static type, leaving the runtime object exactly as it was. They cover most of the shapes an application needs around a domain model:

type UserCreateInput = Pick<User, "email" | "displayName">;
type PublicUser = Omit<User, "roles">;

const draft: UserCreateInput = { email: "ada@example.com", displayName: "Ada" };

console.log(Object.keys(draft));
// prints: [ 'email', 'displayName' ]

The two are not symmetrical in how carefully they check you, which surprises people the first time. Pick constrains its key argument to keyof User, so Pick<User, "emial"> is a compile error. Omit accepts any PropertyKey, that is any string, number, or symbol, so Omit<User, "emial"> is legal and simply removes nothing.

That asymmetry has a practical cost: rename a field on User and every Pick derived from it reports an error, while every Omit quietly starts including the field it was meant to hide. When the omitted field is a password hash or an internal flag, that is worth a test rather than trust.

Omit is also non-distributive: applied to a union it works on the union as a whole, so the result often collapses to the keys the members share. Mapping over the members yourself keeps each branch intact when that matters.

Making Fields Optional or Required

Partial marks every property optional, which is the natural shape of a patch-style update. Required does the reverse, which suits a value that has been through normalization:

type UserPatch = Partial<Pick<User, "email" | "displayName" | "roles">>;

function applyPatch(user: User, patch: UserPatch): User {
  return { ...user, ...patch };
}

const updated = applyPatch(
  { id: "u1", email: "ada@example.com", displayName: null, roles: [], createdAt: new Date() },
  { displayName: "Ada" },
);

console.log(updated.displayName);
// prints: Ada

Composing the two utilities is the common move: pick the patchable fields first, then make them optional, so the patch type cannot smuggle in an id change. Both operate on the top level only, so a nested object inside a Partial keeps its own required fields.

One detail changes with configuration. By default an optional property also accepts an explicit undefined, so { displayName: undefined } is a valid UserPatch and the spread above would overwrite the name with undefined. With exactOptionalPropertyTypes enabled, that assignment is rejected unless the property type includes undefined, which makes the difference between "leave this field alone" and "clear this field" explicit.

Record and Readonly

Record builds an object type from a key type and a value type, and it behaves differently depending on which key type you give it:

type UsersById = Record<string, User>;

type RoleLimits = Record<"admin" | "editor" | "viewer", number>;

const limits: RoleLimits = { admin: 100, editor: 50, viewer: 10 };

console.log(limits.editor);
// prints: 50

With a union of literal keys, every key is required, so adding a fourth role to the union makes each incomplete RoleLimits object an error. That is the version worth reaching for, because it turns a lookup table into an exhaustiveness check. With string as the key type you get an index signature instead, which accepts any key; under noUncheckedIndexedAccess a lookup on it is typed User | undefined, and without the flag it is typed User even for a key that is missing.

Readonly marks each property non-assignable, which documents intent and catches accidental mutation in checked code. It is shallow and compile-time only: nested objects stay writable, and nothing in the emitted JavaScript prevents a change. For arrays, readonly string[] and ReadonlyArray<string> do the equivalent job by removing the mutating methods from the type.

Deriving Types from Functions

Three utilities read a type back out of code you already wrote, which keeps a helper and its consumers in step:

function createSession(userId: string, ttlSeconds: number) {
  return { token: crypto.randomUUID(), userId, expiresIn: ttlSeconds };
}

type Session = ReturnType<typeof createSession>;
type SessionArgs = Parameters<typeof createSession>;

async function loadUser(): Promise<User | null> {
  return null;
}

type LoadedUser = Awaited<ReturnType<typeof loadUser>>;
// LoadedUser is User | null

The typeof here is the type-level operator, not the JavaScript one: it reads the type of the value createSession, which is its function type. Awaited unwraps a promise, including nested ones, which is why it is the reliable way to name what an async function resolves to.

Deriving like this fits helpers whose return shape is genuinely incidental. When the shape is a domain concept that other modules depend on, declare it explicitly and annotate the function with it instead: the error then lands on the function that broke the contract rather than spreading into every consumer.

When to Name the Result

Utility types compose freely, and that is exactly how they become unreadable. The working rule is that an inline composition is fine at one call site, and wants a name the moment it appears twice:

// fine inline, used once
function patchUser(id: string, patch: Partial<Pick<User, "email" | "roles">>): void {}

// used across files: give it a name
type UserContactPatch = Partial<Pick<User, "email" | "roles">>;

A name also gives you somewhere to put the reason. UserContactPatch says which subset of the model it represents; Partial<Pick<User, "email" | "roles">> says only how it was built. The second is fine to read once and tiring to read in five signatures.

Pitfalls and Debugging

An Omit that quietly stopped omitting. Because its key argument is not constrained to the source type's keys, a renamed or misspelled field leaves the utility doing nothing.

Omit edits the static type either way. It strips no runtime properties, and a variable already holding extra fields still assigns to the omitted type, because excess-property checks mainly target fresh object literals. If the omission is a privacy boundary, add a test asserting the field is absent, or use a Pick of the allowed fields instead.

Partial used to avoid modelling states. Making every field optional because the data is uncertain pushes the uncertainty onto every consumer. When the real domain has distinct states, a discriminated union names them, as the unions and narrowing guide covers.

Expecting Readonly at runtime. It is erased with the rest of the type layer. Code reached through any, an assertion, or a plain JavaScript module can mutate the value freely, so treat the marker as documentation with compile-time teeth rather than a guarantee.

Unreadable error messages from nested utilities. Errors on composed types can print the whole expansion. Naming the intermediate type usually shortens the message enough to read, and hovering the alias in an editor shows the resolved shape when you do need it.

Frequently Asked Questions

Why does Omit accept a key that does not exist?

Because of how it is declared. Pick constrains its key parameter to keyof T, so a typo is an error, while Omit accepts any PropertyKey, that is any string, number, or symbol, and removes nothing when the name does not match. A rename can therefore leave a silently useless Omit behind.

Does Partial apply to nested objects?

No. Partial adds an optional marker to the properties of the type you pass and stops there, so an object property keeps all of its own required fields. Making a whole tree optional needs a recursive type you write yourself, and that is usually a signal the model wants splitting rather than loosening.

Does Readonly stop the value changing at runtime?

No. Readonly is a compile-time marker: assigning to a marked property is a type error, and the emitted JavaScript is unchanged. It is also shallow, so nested objects stay writable. Freezing a value at runtime needs Object.freeze, which is a separate mechanism with its own shallow limit.

Should you derive types or write them out?

Derive when one shape is genuinely defined by another, such as a create-input built from a domain model, because deriving keeps them in step through renames. Write it out when the two shapes are independent and only happen to look alike today, since a derived type ties them together and future edits will fight it.

Sources

  1. [1]
    Utility Types
    (typescriptlang.org)
  2. [2]
    Mapped Types
    (typescriptlang.org)
  3. [3]
    TSConfig Reference
    (typescriptlang.org)