Utility Types: Shaping Objects (Part 1)

Learn the core object-shaping utility types built into TypeScript: Partial, Required, Readonly, Record, Pick, Omit, Exclude, and NonNullable, and when to reach for each one.

TypeScript ships a set of generic utility types that cover the most common ways teams reshape an existing type — making fields optional or required, locking them down, selecting or dropping a subset, or narrowing a union. This first half covers the eight utilities most directly concerned with the shape of objects and unions of allowed values, each of which is really just a mapped or conditional type under the hood, shipped so you don't have to write it yourself.

Partial, Required, and Readonly: adjusting modifiers

Partial<T> makes every property of T optional, and is built from { [K in keyof T]?: T[K]; }. It's the standard type for a PATCH-style update payload or an in-progress form draft, where not every field is present yet. Required<T> does the opposite, using the -? modifier to force every property to be present, which is useful after a validation or default-filling step has guaranteed nothing is missing.

Readonly<T> adds the readonly modifier to every property with { readonly [K in keyof T]: T[K]; }, preventing reassignment after the object is created. It's commonly applied to constants, configuration objects, and Redux-style state, where mutation should be prevented at compile time rather than only by convention.

All three are shallow: they only affect the object's own top-level properties, not the properties of any nested objects inside it. A Readonly<T> where T has a nested object property still allows that nested object's own fields to be reassigned, since the outer readonly only locks the outer key from being pointed at a different object entirely.

• type PartialUser = Partial<User>; // every property optional • type FullUser = Required<PartialUser>; // every property required again • type FrozenUser = Readonly<User>; // every property readonly

Record: building an object type from a key set and a value type

Record<K, T> constructs an object type whose keys are drawn from K (typically a union of string, number, or symbol literal types) and whose values are all of type T. It's defined as { [P in K]: T; } and is the standard tool for describing a lookup object, dictionary, or map-shaped object where every key shares the same value type.

Record is especially useful paired with a union of literal keys to guarantee, at compile time, that every possible key has an entry — Record<"red" | "green" | "blue", string> forces you to provide exactly those three keys with string values, and TypeScript will flag a missing key as an error when the object is assigned to that type.

• type ColorMap = Record<"red" | "green" | "blue", string>;

Pick and Omit: selecting a subset of properties

Pick<T, K> constructs a new type by selecting only the properties listed in K (a union of keys of T) out of T, discarding everything else. It's ideal for deriving a smaller, purpose-specific type from a large interface — a UserSummary type that only needs id and name out of a much larger User interface, for example.

Omit<T, K> is the inverse: it constructs a new type with every property of T except the ones listed in K. It's implemented internally using Pick and Exclude together (Pick<T, Exclude<keyof T, K>>), which is a good illustration of how these utilities compose. Omit is the more common choice when a type has many fields and you only want to drop one or two, since listing what to exclude is shorter than listing everything to keep.

• type UserSummary = Pick<User, "id" | "name">; • type UserWithoutPassword = Omit<User, "password">;

Exclude and NonNullable: narrowing unions

Exclude<T, U> takes a union type T and removes every member that is assignable to U, using the distributive conditional type T extends U ? never : T. It operates on unions of any kind, not just object types, and is commonly used to remove one or two specific cases from a broader union of allowed string or literal values.

NonNullable<T> removes null and undefined from a union type T, and is really just a specialized case of Exclude (Exclude<T, null | undefined>). It's the standard way to express 'this value has already been checked and is guaranteed not to be null or undefined' after a guard clause, narrowing a type like string | null down to plain string.

• type Status = "idle" | "loading" | "error" | "success"; • type ActiveStatus = Exclude<Status, "idle">; // "loading" | "error" | "success" • type DefiniteName = NonNullable<string | null | undefined>; // string

Object-shaping utility types

Partial, Required, and Readonly are shallow — they don't recurse into nested object properties. Record<K, T> requires every key in K to be present when a value is assigned to it, unlike a plain index signature. Omit is implemented as Pick<T, Exclude<keyof T, K>> internally, showing how these utilities compose.

interface User {
  id: number;
  name: string;
  email?: string;
  password: string;
}

type PartialUser = Partial<User>;
type RequiredUser = Required<PartialUser>;
type FrozenUser = Readonly<User>;

type ColorMap = Record<"red" | "green" | "blue", string>;

type UserSummary = Pick<User, "id" | "name">;
type UserWithoutPassword = Omit<User, "password">;

type Status = "idle" | "loading" | "error" | "success";
type ActiveStatus = Exclude<Status, "idle">;

type DefiniteName = NonNullable<string | null | undefined>;

Code Examples

Partial<Task> makes every property of Task optional, so updateTask can be called with any subset of Task's fields, matching how a real PATCH request only sends the fields that changed. A field that doesn't exist on Task at all is still rejected, since Partial only affects modifiers, not which keys are valid. Expected result: Compile error on updateTask(3, { finished: true }): Object literal may only specify known properties, and 'finished' does not exist in type 'Partial<Task>'.

function updateTask(id: number, changes: Partial<Task>): void

Omit<Account, "passwordHash"> derives a safe-to-expose type by removing exactly the sensitive field, and toPublicAccount uses object destructuring to strip it out at runtime to match. Separately, Record forces statusColors to define an entry for every one of the three status keys, catching a missing color at compile time rather than leaving a UI element with no defined color at runtime.

type PublicAccount = Omit<Account, "passwordHash">;

Common mistake: Omit<Account, "passwordHash"> only changes the type; it does not remove passwordHash from the actual object at runtime — toPublicAccount still has to destructure it out manually to match the derived type's real shape.

interface Task {
  id: number;
  title: string;
  done: boolean;
}

function updateTask(id: number, changes: Partial<Task>): void {
  console.log(`Updating task ${id} with`, changes);
}

updateTask(1, { done: true });          // OK, only done provided
updateTask(2, { title: "New title" });  // OK, only title provided
updateTask(3, { finished: true });      // Error: no property 'finished' on Task
interface Account {
  id: string;
  email: string;
  passwordHash: string;
  createdAt: Date;
}

type PublicAccount = Omit<Account, "passwordHash">;

function toPublicAccount(account: Account): PublicAccount {
  const { passwordHash, ...rest } = account;
  return rest;
}

const statusColors: Record<"success" | "warning" | "error", string> = {
  success: "#22c55e",
  warning: "#eab308",
  error: "#ef4444"
  // Error if any key is missing: Property 'error' is missing in type ...
};

Pick vs. Omit on the same source type

Pick and Omit often reach the same resulting shape from opposite directions — choose whichever list (keep vs. drop) is shorter to write for a given type.

type User = { id: number; name: string; password: string }

   Pick<User, "id" | "name">          Omit<User, "password">
           |                                  |
  { id: number; name: string }      { id: number; name: string }

  (Pick lists what to KEEP)        (Omit lists what to DROP)

Important Rules

Partial, Required, and Readonly only affect a type's own top-level properties — they are shallow, not deep.

Record<K, T> requires every key in the union K to be present in the final object type, unlike a plain index signature which allows any number of keys.

Pick keeps only the listed keys; Omit keeps everything except the listed keys — pick whichever list is shorter for a given type.

Exclude and NonNullable operate on unions in general, not just object types; NonNullable is simply Exclude<T, null | undefined>.

Where this shows up

REST API client code leans on Partial for PATCH request bodies, Omit to strip server-only or sensitive fields (like a password hash) before sending data to the client, and Record to type lookup tables such as status-to-color maps or locale-to-translation-string dictionaries. Form libraries use Required after a validation step to convert a type where fields started optional (because the user hasn't filled them in yet) into a guaranteed-complete type once validation passes, so downstream code doesn't have to keep checking for undefined.

type Config = Readonly<{ nested: { value: number } }>; config.nested.value = 5; // assumed this would error

// This assignment actually compiles fine — Readonly is shallow // Use a recursive/deep readonly mapped type if nested immutability is required

Readonly<T> only locks the top-level properties from reassignment; config.nested itself can't be reassigned to a different object, but config.nested.value can still be mutated freely, since the nested object's own properties were never made readonly.

const colors: Record<Status, string> = { idle: "gray", loading: "blue" }; // missing keys silently allowed const colors: Record<Status, string> = { idle: "gray", loading: "blue", error: "red", success: "green" };

Record requires every key from the key union to be present; a partial object literal assigned to a Record type is a compile error listing the missing keys, not a silently accepted subset.

[multiple-choice] What is the key difference between Pick<T, K> and Omit<T, K>? A. They are exactly the same B. Pick keeps only the listed keys; Omit keeps everything except the listed keys C. Pick works on unions, Omit only works on objects D. Omit makes properties optional, Pick makes them required

B. Pick keeps only the listed keys; Omit keeps everything except the listed keys — Pick<T, K> constructs a type from only the keys listed in K, while Omit<T, K> constructs a type from every key of T except the ones listed in K.

[predict-output] Given const colors: Record<"a" | "b", string> = { a: "red" };, what happens at compile time? A. It compiles fine, b defaults to undefined B. A compile error because property 'b' is missing C. It compiles but colors.b is any at runtime D. Record silently ignores missing keys

B. A compile error because property 'b' is missing — Record<K, T> requires every key in K to be present in the assigned object; omitting 'b' is a compile-time error listing it as a missing property.

[multiple-choice] Why is Readonly<T> called 'shallow'? A. It only works on primitive types B. It only makes the top-level properties readonly, not properties of nested objects C. It removes half of T's properties D. It only works with interfaces, not type aliases

B. It only makes the top-level properties readonly, not properties of nested objects — Readonly<T> adds the readonly modifier only to T's own top-level keys; any nested object's own properties remain fully mutable unless separately wrapped in Readonly (or a deep/recursive readonly type).

Given interface Invoice { id: string; amount: number; paidAt?: Date; internalNotes: string; }, write: a PublicInvoice type that omits internalNotes, a type InvoiceDraft that makes every field optional, and a Record type mapping the three strings "draft" | "sent" | "paid" to a string label.

interface Invoice { id: string; amount: number; paidAt?: Date; internalNotes: string; } type PublicInvoice = Omit<Invoice, "internalNotes">; type InvoiceDraft = Partial<Invoice>; type InvoiceStatusLabel = Record<"draft" | "sent" | "paid", string>;

Use Omit for PublicInvoice, Partial for InvoiceDraft, and Record<"draft" | "sent" | "paid", string> for the label map.