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

Published Updated

TypeScript utility types let you reuse an existing type with a few changes. You can select fields with Pick, make properties optional with Partial, or read a function’s return type with ReturnType. That saves you from copying fields and updating them in several places.

These utilities build on TypeScript generics. Start with the handful you need, and give complicated combinations a readable name. The TypeScript guide covers the surrounding language basics.

What Are TypeScript Utility Types?

TypeScript utility types create new types from existing ones. Most 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

ReturnType, Parameters, and Awaited extract types from existing functions and promises. See typing functions and callbacks for the function signatures they work with:

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 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 Should You Name a Derived Type?

Give a derived TypeScript type a name when you reuse it or when the combination is hard to read. A short utility type can stay inline at a single call site:

// 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. To keep private fields out of a response, construct a new object containing only the allowed fields and test the actual output. Neither Omit nor Pick removes runtime data.

Partial used to avoid modeling 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 use runtime checks or freezing when you need to prevent changes while the program runs.

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 TypeScript Omit accept a key that does not exist?

TypeScript Omit accepts any string, number, or symbol as its key parameter, even when that key is absent from the source type. A misspelled key therefore removes nothing. Pick checks its keys against keyof T and reports the same typo as an error.

Does TypeScript Partial apply to nested objects?

TypeScript Partial makes only the top-level properties optional. A nested object keeps its required fields. To make nested properties optional too, define the nested update types explicitly or use a recursive utility suited to your data.

Does TypeScript Readonly prevent changes at runtime?

TypeScript Readonly does not prevent changes at runtime. It reports assignments to readonly properties during type checking and leaves the emitted JavaScript unchanged. It is shallow, so nested objects remain writable. Object.freeze can prevent runtime changes to an object’s own properties, but it is also shallow.

Should you derive TypeScript types or write them out?

Derive a TypeScript type when it should change with another type, such as an update input based on a user model. Write separate types when they represent independent data, even if their fields happen to match today.

Sources

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