codejump

AcademyTypeScript

Partial, Pick, Omit and the rest

Partial, Pick, Omit, Record, Required, Readonly and the rest — what each one is for, which one is unsafe, and how to stop re-declaring the same shape.

3 min readUpdated

3 exercises

Waiting at the end of this page.

One shape, many views

A typical entity is written once and then needed in four variations: the full row, the fields you may create, the fields you may update, the fields you may show. Writing those by hand means four places to edit and three chances to forget.

TypeScript
interface User {
  id: string;
  email: string;
  password: string;
  createdAt: Date;
}

type NewUser = Omit<User, 'id' | 'createdAt'>;
type UserUpdate = Partial<NewUser>;
type PublicUser = Omit<User, 'password'>;

Every one of those follows User. That is the whole point.

The seven you will use constantly

TypeDoesNote
Partial<T>every field optionalfor updates and patches
Required<T>every field requiredstrips ? — including ones you wanted
Readonly<T>every field readonlyshallow, one level only
Pick<T, K>keep these keysfails if a key does not exist
Omit<T, K>drop these keysdoes not fail on a key that does not exist
Record<K, V>an object from keys to a valueRecord<Role, boolean>
ReturnType<F>what a function gives backpairs with typeof someFn

Omit is the one that lies to you

Pick constrains its keys to keyof T. Omit does not — by design, so you can omit keys from a union. The cost is that a typo passes silently:

TypeScript
type PublicUser = Omit<User, 'passwrod'>;
// No error. And 'password' is still in there.

That is a real leak, from a typo the compiler was willing to catch anywhere else. When the omitted field is one that must not escape, say it in a way that fails loudly:

TypeScript
type StrictOmit<T, K extends keyof T> = Omit<T, K>;

type PublicUser = StrictOmit<User, 'passwrod'>;
// Error: '"passwrod"' does not satisfy the constraint 'keyof User'.

Three words of type, and the typo is a build failure again.

Readonly stops at the first level

TypeScript
const config: Readonly<{ db: { host: string } }> = { db: { host: 'a' } };

config.db = { host: 'b' };  // Error
config.db.host = 'b';       // Fine. Nothing stopped this.

Readonly maps over the top-level keys and no further. For a nested structure you need a recursive version — and remember that all of it vanishes at runtime. If the object must actually be immutable while it is running, you want Object.freeze, applied recursively, as well as the type.

Exclude and Extract work on unions, not objects

These two are the ones people reach for on an object shape and get nothing from. They filter union members:

TypeScript
type Status = 'idle' | 'loading' | 'done' | 'failed';

type Settled = Exclude<Status, 'idle' | 'loading'>;   // 'done' | 'failed'
type Pending = Extract<Status, 'idle' | 'loading'>;   // 'idle' | 'loading'

type Defined<T> = Exclude<T, null | undefined>;

NonNullable<T> is that last one, already in the standard library.

Composing them is the point

TypeScript
type Handlers = Record<
  `on${Capitalize<Status>}`,
  (result: PublicUser) => void
>;
// { onIdle: ..., onLoading: ..., onDone: ..., onFailed: ... }

Each utility is a few lines of mapped or conditional type — nothing you could not write yourself, which is exactly why they are worth knowing: when one does not fit, you can write the one that does. Omit itself is Pick<T, Exclude<keyof T, K>>, and reading it that way is the moment the whole set stops being a list to memorise.

Now practice it

2 challenges · 1 quiz

Reading this page is the cheap half. These are the exercises that make you use it.