TypeScript Utility Types: Partial, Pick, Omit, Record — and How They Work

October 5, 2026 · 3 min read

A real app needs several versions of the same shape: the full User, a patch where every field is optional, a public view without the email, a preview with just the id and name. Writing each one by hand works until someone adds a field to User and forgets the other four.

Utility types derive the variations from the original, so they stay in sync automatically.

type User = { id: number; name: string; email: string }
 
type Partial<T> = { [K in keyof T]?: T[K] } // built in
 
type UserPatch = Partial<User>
type PublicUser = Omit<User, 'email'>
type UserPreview = Pick<User, 'id' | 'name'>
type FrozenUser = Readonly<User>
type UsersById = Record<number, User>
User
idnumber
namestring
emailstring

Start with one type. Utility types build new types from it, so related types never drift out of sync with the original.

0 / 6

The everyday utility types

type User = { id: number; name: string; email: string };
UtilityResultUse it for
Partial<User>every property optionalupdate/patch payloads
Required<User>every property requiredafter defaults are filled in
Readonly<User>every property readonlyvalues that must not be mutated
Pick<User, 'id' | 'name'>only id and namepreviews, list items
Omit<User, 'email'>everything except emailpublic responses
Record<string, User>an object with string keys and User valueslookup tables
function updateUser(id: number, patch: Partial<User>) { … }
updateUser(1, { name: 'Grace' });          // ✓ any subset of fields

function toPublic(user: User): Omit<User, 'email'> {
  return { id: user.id, name: user.name };
}

And a few that work on functions and unions:

type Params = Parameters<typeof updateUser>;    // [id: number, patch: Partial<User>]
type Out = ReturnType<typeof toPublic>;         // Omit<User, 'email'>
type Data = Awaited<Promise<User>>;             // User
type Status = 'idle' | 'loading' | 'error';
type Busy = Exclude<Status, 'idle'>;            // 'loading' | 'error'
type NotNull = NonNullable<string | null>;      // string

How they're built: mapped types

None of these are magic. Most are a few characters of ordinary TypeScript built from three pieces:

keyof T — the union of T's property names:

type Keys = keyof User;   // 'id' | 'name' | 'email'

T[K] — the type of property K in T (an indexed access type):

type Name = User['name'];   // string

[K in Keys] — a mapped type: loop over a union of keys and produce one property for each.

Put together:

type Partial<T>  = { [K in keyof T]?: T[K] };
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Record<K extends keyof any, V> = { [P in K]: V };

Read Partial aloud: "for each key K of T, make an optional property K whose type is whatever T[K] is." Omit is Pick with the excluded keys removed: Pick<T, Exclude<keyof T, K>>.

Writing your own

Once you see the pattern, you can make your own:

// Every property becomes nullable
type Nullable<T> = { [K in keyof T]: T[K] | null };

// A form's error messages: same keys, string values
type FormErrors<T> = { [K in keyof T]?: string };

const errors: FormErrors<User> = { email: 'Invalid email' };   // ✓
const bad: FormErrors<User> = { emial: 'typo' };               // ✗ caught

FormErrors<User> will pick up new fields the moment User gets them — and reject typos in field names.

Tips

  • Derive, don't duplicate. If two types share fields, one should probably be built from the other.
  • Omit doesn't check that the key exists: Omit<User, 'emial'> compiles silently. When it matters, Pick the fields you want instead.
  • These are compile-time only. Readonly<User> doesn't freeze anything at runtime; Object.freeze does.
  • Hover over a derived type in your editor to see what it resolved to. It's the fastest way to understand a type someone else wrote.

The takeaway

Utility types let one definition be the source of truth for a whole family of shapes. And underneath, they're just keyof, T[K] and [K in …] — a small loop that runs over types instead of values.