Community Article

TypeScript Utility Types I Use Every Week

The seven utility types that show up in every PR (Partial, Required, Pick, Omit, Record, Readonly, ReturnType), plus the three I keep on standby (Awaited, Parameters, NonNullable/Exclude/Extract). When to derive instead of restate.

TypeScript Utility Types I Use Every Week

The seven utility types that show up in every PR (Partial, Required, Pick, Omit, Record, Readonly, ReturnType), plus the three I keep on standby (Awaited, Parameters, NonNullable/Exclude/Extract). When to derive instead of restate.

ts-utility-types
ts-mapped-types
type-system
fundamentals
interview-prep
diyaandersen

By @diyaandersen

November 24, 2025

·

Updated May 18, 2026

344 views

4

4.3 (14)

A teammate asked me last quarter to count the utility types I had used in the previous PR. The answer was nine. Nine utility types in one PR, none of them remarkable, all of them pulling their weight: Pick, Omit, Partial, ReturnType, Awaited, Record, NonNullable, Parameters, and Readonly. The reason it stuck with me is that five years earlier, the same PR would have had any in three places and a hand-rolled interface for every transformation. The utility types were not novel; they had been there for years. The shift was that I had finally learned which ones to reach for in which situations.

The argument I want to make is that utility types are not a separate API to memorise; they are the working set of a typed codebase. Once you can name the seven or eight that show up most often, the rest fall out as compositions. This piece is the rotation I cycle through every week, with the actual cases that put each one in my hands.

The seven I use weekly

Listed in roughly the order they appear in the PRs I review.

UtilityWhat it doesWhen I reach for it
Partial<T>Makes every property optionalUpdate payloads, "patch" methods, options merging
Required<T>Makes every property requiredRe-narrowing after a Partial round trip
Pick<T, K>Keep only the listed keysPublic-facing DTOs derived from internal types
Omit<T, K>Remove the listed keysStrip server-controlled fields from create payloads
Record<K, V>Map type with K keys, V valuesLookup tables, dictionaries by union of literal keys
Readonly<T>Mark all properties readonlyGuard frozen state, redux-style stores
ReturnType<F>The return type of a functionInferring action payloads, hook results, factory outputs

These seven cover, by my rough count, 80 percent of the utility-type uses in the codebases I work in. The remaining 20 percent are the ones I use less often but hit the right note when they appear (Awaited, Parameters, NonNullable, Exclude, Extract).

Partial and Required: the optional-property pair

Partial<T> rebuilds T with every property marked optional. The classic application is "update" payloads: a function that lets you update any subset of fields on an entity.

interface User {
    id: string;
    name: string;
    email: string;
    role: 'admin' | 'member';
}

function updateUser(id: string, patch: Partial<User>) {
    // patch could be { name: 'Alice' } or {} or any subset
}

The pair to know is Required<T>, which forces every property to be present. The combination shows up in form-handling code: a draft starts as Partial<User>, and the validator turns it into Required<User> only after every field has been confirmed non-null.

function validate(draft: Partial<User>): Required<User> | null {
    if (!draft.id || !draft.name || !draft.email || !draft.role) return null;
    return draft as Required<User>;
}

The cast at the end is honest. The validator did the runtime work; the type cast is the compiler's confirmation that the runtime check was sufficient. This pattern is the cleanest way I have found to bridge "untrusted input" to "validated, fully-populated record" without writing a parallel type for the validated form.

Pick and Omit: subset and complement

Pick<T, K> keeps only the named keys; Omit<T, K> drops them. They are the same operation expressed two ways. I reach for whichever makes the call site shorter.

type PublicUser = Pick<User, 'id' | 'name' | 'role'>;
type CreateUserInput = Omit<User, 'id'>;     // server assigns the id
type UpdateUserInput = Omit<Partial<User>, 'id'>;

The composition Omit<Partial<T>, K> is something I write multiple times a week. "All optional, but the server owns these specific keys" is the shape of nearly every PATCH endpoint I have ever written.

The mistake I see in code review is hand-rolling these subsets:

// Don't do this
interface PublicUser {
    id: string;
    name: string;
    role: 'admin' | 'member';
}

Two parallel interfaces, drift waiting to happen. Add avatarUrl to User and you have to remember to add it to PublicUser, and you only catch the mismatch at runtime if your tests are thorough. The Pick derivation makes the relationship explicit at the type level.

Record: the dictionary type

Record<K, V> is the type of an object with keys of type K and values of type V. The killer use is mapping a union of string literals to a value type:

type RoutePathByName = Record<'home' | 'profile' | 'settings', string>;
const paths: RoutePathByName = {
    home: '/',
    profile: '/profile',
    settings: '/settings',
    // forgetting any of the three is a compile error
};

If you forget the home key, the compiler tells you. If you add a key that is not in the union, the compiler tells you. The exhaustiveness check is automatic.

Record<string, V> is the looser dictionary form: any string key, with all values of type V. I treat this as a code smell when I see it. If the keys really are arbitrary strings, the type is honest; if the keys are actually a known union, switch to the literal form so the compiler can help you.

Readonly: the immutability marker

Readonly<T> marks every property as readonly at compile time. The runtime is unaffected; the marker is purely a check that the compiler enforces.

function freeze<T>(obj: T): Readonly<T> {
    return Object.freeze(obj) as Readonly<T>;
}

const config = freeze({ region: 'us-east-1', tier: 'prod' });
config.region = 'eu-west-1';  // compile error

The pattern I use in shared state stores: declare the public type as Readonly<T>, declare the internal mutable type as plain T, and only cross the boundary in the store's privileged update functions. Consumers cannot mutate the state by accident; the compiler refuses.

ReadonlyArray<T> and ReadonlyMap<K, V> are the array and map equivalents. Same idea: the immutable view is a different type from the mutable view, even though the runtime object is identical.

ReturnType and Parameters: introspecting functions

ReturnType<F> extracts the return type of F. Parameters<F> extracts the parameter list as a tuple type. Both are invaluable when you need to derive types from existing function signatures rather than restate them.

function createUser(input: CreateUserInput) {
    return { id: crypto.randomUUID(), ...input };
}

type CreatedUser = ReturnType<typeof createUser>;
// CreatedUser is { id: string; name: string; email: string; role: ... }

The pattern shows up in test fixtures, in Redux reducer types (where the action type is derived from the action creator's return type), and in higher-order-function compositions where the inner function's signature drives the outer one.

function logged<F extends (...args: any[]) => any>(fn: F): F {
    return ((...args: Parameters<F>) => {
        console.log('called with', args);
        return fn(...args);
    }) as F;
}

The Parameters<F> here is what makes the wrapper preserve the original signature without restating it.

Awaited: unwrapping promise types

Async functions return Promise<T>. Sometimes you need the T without the wrapper: a callback type that consumes whatever the async function produced, a derived type that mirrors the resolved shape. Awaited<P> is the utility:

async function fetchUser(id: string) {
    const res = await fetch(`/api/users/${id}`);
    return res.json() as Promise<User>;
}

type FetchedUser = Awaited<ReturnType<typeof fetchUser>>;   // User

Awaited recursively unwraps nested promises, which matters because a function that returns Promise<Promise<T>> (rare but possible) should still surface as T at the consumer's level. The recursive behavior is documented; you do not have to chain Awaited manually.

NonNullable, Exclude, Extract: union surgery

These three operate on union types. NonNullable<T> removes null and undefined from a union. Exclude<T, U> removes anything assignable to U from T. Extract<T, U> keeps only the parts of T assignable to U.

type Status = 'pending' | 'active' | 'archived' | null;
type LiveStatus = NonNullable<Status>;                     // 'pending' | 'active' | 'archived'
type FinalStatus = Exclude<Status, 'pending' | null>;      // 'active' | 'archived'
type DraftStatus = Extract<Status, 'pending' | null>;       // 'pending' | null

In API code, these three appear constantly. A handler that has narrowed away the null case wants its parameter typed as NonNullable<Status>. A switch over a smaller subset of the union wants Extract. A function that handles "all but the pending case" wants Exclude.

What I avoid: abusing utility types as logic

Utility types compose, and the composition can get clever. I have seen single type aliases that are a five-deep nest of Pick, Omit, Partial, Record, and conditional types, and the result is unreadable even with the IDE's hover preview.

The check I run on my own code: if the type alias takes more than two seconds to read aloud, it should be split into named intermediate types. The compiler does not care about the difference between one nested alias and three named ones; humans do.

// Hard to read
type X = Partial<Pick<Omit<User, 'id' | 'createdAt'>, 'name' | 'role'>>;

// Easier to read
type WritableUser = Omit<User, 'id' | 'createdAt'>;
type EditableField = Pick<WritableUser, 'name' | 'role'>;
type EditableUserPatch = Partial<EditableField>;

Same type, three names. The PR reviewer who reads the second version six months from now will appreciate the breadcrumbs.

The rotation, condensed

Every week, I cycle through roughly this set of moves:

  • Derive shapes from sources of truth. Pick, Omit, Partial, Required instead of parallel interfaces.
  • Map literal-key unions to values. Record<'a' | 'b' | 'c', V> instead of an interface that has to be kept in sync with the union.
  • Read function and promise types from existing values. ReturnType<typeof fn>, Parameters<typeof fn>, Awaited<...>.
  • Surge unions with NonNullable, Exclude, Extract when narrowing past null or partitioning the cases.
  • Mark immutability with Readonly at the boundaries that should not be mutated.

Five moves, seven utilities at the core, three more for surgery. The whole working set fits on a single sticky note. The win is consistency: every codebase that uses these types in the same way is a codebase you can read at a glance. The lose is when teams hand-roll the same shapes and produce drifted parallel interfaces nobody trusts.

The rotation has not changed much in five years. The utilities were already good. The change was learning which ones to reach for first, and that is the kind of muscle memory that pays back every Monday.

Back to Articles