Skip to content
TR

Phase 6 · Utility types in practice · Lesson 6.1

Advanced

The built-in utility types

Partial, Pick, Omit, Record, ReturnType, Awaited, NoInfer and the rest: what each one does, the gotchas interviewers ask about, and how to pick the right one.

25 min

You have a User type. The update endpoint takes "a User, but every field optional". The public profile is "a User without the password". The cache is "a map from user id to User". If you write each of those by hand, they drift the day someone adds a field to User. Utility types derive new types from one source of truth, so they can't drift.

TypeScript ships about twenty of them globally, no import needed. This lesson walks through all of them, grouped by job, with the edge cases that come up in interviews.

Changing modifiers: Partial, Required, Readonly

These three keep every key and change only the ? and readonly modifiers.

type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
 
type User = { id: number; name: string; email?: string };
 
type UserPatch = Partial<User>;   // every key optional
type FullUser = Required<User>;   // every key required
type FrozenUser = Readonly<User>; // every key readonly
 
type _t1 = Expect<Equal<UserPatch, { id?: number; name?: string; email?: string }>>;
type _t2 = Expect<Equal<FullUser, { id: number; name: string; email: string }>>;
type _t3 = Expect<Equal<FrozenUser, { readonly id: number; readonly name: string; readonly email?: string }>>;

Notice Required also removed undefined from email: an optional property implicitly allows undefined, and Required strips that along with the ?.

The classic use is a PATCH function:

type User = { id: number; name: string; email: string };
 
function updateUser(current: User, changes: Partial<User>): User {
  return { ...current, ...changes };
}
 
const ada: User = { id: 1, name: "Ada", email: "ada@example.com" };
updateUser(ada, { name: "Ada Lovelace" });
// @ts-expect-error -- Object literal may only specify known properties, and 'nmae' does not exist in type 'Partial<User>'.
updateUser(ada, { nmae: "typo" });
type Account = { owner: { name: string }; tags: string[] };
const acct: Readonly<Account> = { owner: { name: "Ada" }, tags: [] };
 
// @ts-expect-error -- Cannot assign to 'owner' because it is a read-only property.
acct.owner = { name: "Grace" };
 
acct.owner.name = "Grace"; // compiles: Readonly is shallow
acct.tags.push("admin");   // compiles too

Quick check

Which line in the snippet is a type error?

type Config = { settings: { theme: string }; plugins: string[] };
declare const cfg: Readonly<Config>;
 
cfg.settings = { theme: "dark" };  // A
cfg.settings.theme = "dark";       // B
cfg.plugins.push("search");        // C

Choosing keys: Pick and Omit

Pick<T, K> keeps only the keys in K; Omit<T, K> keeps everything except them.

type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
 
type User = { id: number; name: string; password: string };
 
type Credentials = Pick<User, "name" | "password">;
type PublicUser = Omit<User, "password">;
 
type _t1 = Expect<Equal<Credentials, { name: string; password: string }>>;
type _t2 = Expect<Equal<PublicUser, { id: number; name: string }>>;
 
// @ts-expect-error -- Type '"nmae"' does not satisfy the constraint 'keyof User'.
type Broken = Pick<User, "nmae">;

Pick constrains K extends keyof T, so a typo is an error. Omit does not. Its definition in lib.es5.d.ts is:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

K extends keyof any means "any string, number or symbol". That's deliberate (it lets you omit keys generically without proving they exist), but it has two consequences you must know.

Pitfall 1: Omit accepts keys that don't exist

type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
 
type User = { id: number; name: string; password: string };
 
type PublicUser = Omit<User, "pasword">; // typo: no error...
type _t1 = Expect<Equal<PublicUser, User>>; // ...and the password is still there

The fix is a stricter wrapper that constrains the keys:

type StrictOmit<T, K extends keyof T> = Omit<T, K>;
 
type User = { id: number; name: string; password: string };
 
type PublicUser = StrictOmit<User, "password">; // fine
// @ts-expect-error -- Type '"pasword"' does not satisfy the constraint 'keyof User'.
type Typo = StrictOmit<User, "pasword">;

Pitfall 2: Omit is not distributive over unions

keyof (A | B) is only the keys common to both members. So Omit on a union first collapses it to its shared keys, and you silently lose the variant-specific ones.

type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
 
type Circle = { kind: "circle"; radius: number; id: string };
type Square = { kind: "square"; size: number; id: string };
type Shape = Circle | Square;
 
type NewShape = Omit<Shape, "id">;
type _t1 = Expect<Equal<NewShape, { kind: "circle" | "square" }>>; // radius and size are gone!

The fix is a distributive conditional type: T extends unknown ? ... : never applies the operation to each union member separately.

type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
 
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
 
type Circle = { kind: "circle"; radius: number; id: string };
type Square = { kind: "square"; size: number; id: string };
type Shape = Circle | Square;
 
type NewShape = DistributiveOmit<Shape, "id">;
type _t1 = Expect<Equal<NewShape, { kind: "circle"; radius: number } | { kind: "square"; size: number }>>;
 
function create(shape: NewShape) {
  if (shape.kind === "circle") return shape.radius; // narrowing still works
  return shape.size;
}

▶ Try it in the TypeScript Playground

Pick has the same non-distributive behaviour, but it's rarer to hit because its key constraint already forces you to use common keys.

Quick check

Given the Shape union above, what is Omit<Shape, "radius">?

Building maps: Record

Record<K, V> is an object type whose keys are K and whose values are V.

type Status = "active" | "banned" | "pending";
 
const labels: Record<Status, string> = {
  active: "Active",
  banned: "Banned",
  pending: "Pending",
};
 
// @ts-expect-error -- Property 'pending' is missing in type '{ active: string; banned: string; }' but required in type 'Record<Status, string>'.
const incomplete: Record<Status, string> = { active: "Active", banned: "Banned" };
 
const partialLabels: Partial<Record<Status, string>> = { active: "Active" }; // some keys only

Two things to remember:

  • A union of literal keys makes every key required. That's a feature: add a new Status and the compiler lists every map you forgot to update. Wrap in Partial when you really want "some of them".
  • Record<string, V> lies about lookups. cache["anything"] has type V, not V | undefined, unless you enable noUncheckedIndexedAccess. Prefer a Map or turn that flag on for dictionaries with unknown keys.

Filtering unions: Exclude, Extract, NonNullable

These operate on unions, not objects.

type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
 
type Event =
  | { type: "click"; x: number; y: number }
  | { type: "key"; key: string }
  | { type: "scroll"; offset: number };
 
type NotScroll = Exclude<Event["type"], "scroll">;       // remove members
type KeyEvent = Extract<Event, { type: "key" }>;         // keep members assignable to U
type Id = NonNullable<string | number | null | undefined>; // drop null and undefined
 
type _t1 = Expect<Equal<NotScroll, "click" | "key">>;
type _t2 = Expect<Equal<KeyEvent, { type: "key"; key: string }>>;
type _t3 = Expect<Equal<Id, string | number>>;

Exclude<T, U> is T extends U ? never : T, and Extract is the mirror. Both distribute over T, which is why they work member by member. Extract<Event, { type: "key" }> is the idiomatic way to pull one variant out of a discriminated union.

Since TypeScript 4.8, NonNullable<T> is defined as T & {}: intersecting with {} ("any non-nullish value") removes null and undefined.

Reading functions: ReturnType and Parameters

These take a function type and pull out its pieces. Because they take a type, you almost always pair them with typeof:

type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
 
function createUser(name: string, age?: number) {
  return { id: crypto.randomUUID(), name, age: age ?? 0 };
}
 
type NewUser = ReturnType<typeof createUser>;
type CreateArgs = Parameters<typeof createUser>;
type FirstArg = Parameters<typeof createUser>[0];
 
type _t1 = Expect<Equal<NewUser, { id: `${string}-${string}-${string}-${string}-${string}`; name: string; age: number }>>;
type _t2 = Expect<Equal<CreateArgs, [name: string, age?: number]>>;
type _t3 = Expect<Equal<FirstArg, string>>;

This is how you type things you don't own: a library returns an object with no exported type? ReturnType<typeof thatFunction> names it for you. (crypto.randomUUID() returns a template literal type, which is why id looks the way it does.)

Parameters returns a labelled tuple, so you can forward arguments safely:

function log(level: "info" | "error", message: string) {
  console.log(`[${level}] ${message}`);
}
 
function logTwice(...args: Parameters<typeof log>) {
  log(...args);
  log(...args);
}
 
logTwice("info", "hello");
// @ts-expect-error -- Argument of type '"debug"' is not assignable to parameter of type '"info" | "error"'.
logTwice("debug", "hello");

Gotcha: overloads

For an overloaded function, ReturnType and Parameters only see the last overload signature. They can't pick "the right one" because there's no call to resolve.

type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
 
function parse(input: string): number;
function parse(input: number): string;
function parse(input: string | number) {
  return typeof input === "string" ? Number(input) : String(input);
}
 
type R = ReturnType<typeof parse>;
type _t1 = Expect<Equal<R, string>>; // last overload only

Quick check

What is R?

function check(x: string): string;
function check(x: number): number;
function check(x: boolean): boolean;
function check(x: unknown) {
  return x;
}
 
type R = ReturnType<typeof check>;

Reading classes: ConstructorParameters and InstanceType

A class name used as a value is the constructor; typeof MyClass is the constructor's type. These two helpers read it:

type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
 
class HttpClient {
  constructor(public baseUrl: string, public timeoutMs = 5000) {}
}
 
type ClientArgs = ConstructorParameters<typeof HttpClient>;
type Client = InstanceType<typeof HttpClient>;
 
type _t1 = Expect<Equal<ClientArgs, [baseUrl: string, timeoutMs?: number]>>;
type _t2 = Expect<Equal<Client, HttpClient>>;
 
// A generic factory: works for any class
function make<C extends new (...args: any[]) => any>(Ctor: C, ...args: ConstructorParameters<C>): InstanceType<C> {
  return new Ctor(...args);
}
 
const client = make(HttpClient, "https://api.example.com");
//    ^? const client: HttpClient

InstanceType<typeof HttpClient> is just HttpClient, so it's pointless when you have the class name. It shines in generic code like make above, where all you have is a constructor type. Both helpers also accept abstract constructors.

Unwrapping promises: Awaited

Awaited<T> models what await does: it unwraps promises recursively, and leaves non-promise types alone.

type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
 
type _t1 = Expect<Equal<Awaited<Promise<string>>, string>>;
type _t2 = Expect<Equal<Awaited<Promise<Promise<number>>>, number>>;
type _t3 = Expect<Equal<Awaited<boolean | Promise<string>>, boolean | string>>;
 
async function fetchUser() {
  return { id: 1, name: "Ada" };
}
 
type User = Awaited<ReturnType<typeof fetchUser>>; // the most common combo
type _t4 = Expect<Equal<User, { id: number; name: string }>>;

Awaited<ReturnType<typeof fn>> is a pattern worth memorising: "the type this async function resolves to".

Controlling inference: NoInfer (TypeScript 5.4)

When a type parameter appears in several places, TypeScript infers it from all of them. Sometimes you want one position to be checked against the type, not to widen it.

function pickColor<C extends string>(colors: C[], fallback: C) {
  return colors[0] ?? fallback;
}
 
pickColor(["red", "green"], "blue"); // no error: C was inferred as "red" | "green" | "blue"

NoInfer<T> tells the compiler "don't use this spot as an inference candidate":

function pickColor<C extends string>(colors: C[], fallback: NoInfer<C>) {
  return colors[0] ?? fallback;
}
 
pickColor(["red", "green"], "green"); // fine
// @ts-expect-error -- Argument of type '"blue"' is not assignable to parameter of type '"red" | "green"'.
pickColor(["red", "green"], "blue");

▶ Try it in the TypeScript Playground

Before 5.4, people used a second type parameter (<C extends string, F extends C>) for the same effect. NoInfer is clearer.

this helpers: ThisParameterType, OmitThisParameter, ThisType

A function can declare the type of this with a fake first parameter. Two helpers read and strip it:

type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
 
function toHex(this: number) {
  return this.toString(16);
}
 
type _t1 = Expect<Equal<ThisParameterType<typeof toHex>, number>>;
type _t2 = Expect<Equal<OmitThisParameter<typeof toHex>, () => string>>;
 
const hexOf255: OmitThisParameter<typeof toHex> = toHex.bind(255);
hexOf255(); // "ff"

ThisType<T> is different: it's a marker with no structure of its own. Inside an object literal whose contextual type includes ThisType<T>, this in methods is typed as T. It's how "options object" APIs type this (it requires noImplicitThis, which strict enables):

type Options<D, M> = { data: D; methods: M & ThisType<D & M> };
 
function define<D, M>(options: Options<D, M>): D & M {
  return { ...options.data, ...options.methods };
}
 
const counter = define({
  data: { count: 0 },
  methods: {
    increment() {
      this.count++; // `this` is { count: number } & { increment(): void }
    },
  },
});
counter.increment();

You'll rarely write ThisType yourself; it's enough to recognise it.

Intrinsic string types

Four utilities transform string literal types. They're intrinsic: implemented inside the compiler, not in lib.d.ts.

type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
 
type _t1 = Expect<Equal<Uppercase<"get">, "GET">>;
type _t2 = Expect<Equal<Lowercase<"POST">, "post">>;
type _t3 = Expect<Equal<Capitalize<"click">, "Click">>;
type _t4 = Expect<Equal<Uncapitalize<"UserId">, "userId">>;
 
type EventName = "click" | "focus";
type Handler = `on${Capitalize<EventName>}`; // distributes over the union
type _t5 = Expect<Equal<Handler, "onClick" | "onFocus">>;

They shine combined with template literal types and key remapping (as), which the mapped-types lessons cover.

Spot the error

A teammate wants "the type of the user that loadUser resolves to". This has two mistakes:

async function loadUser(id: number) {
  return { id, name: "Ada" };
}
 
type User = ReturnType<loadUser>;
Show the fix
  1. loadUser is a value, and ReturnType needs a type. The error is 'loadUser' refers to a value, but is being used as a type here. Did you mean 'typeof loadUser'?
  2. Even with typeof, the return type of an async function is a Promise. Unwrap it with Awaited.
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
 
async function loadUser(id: number) {
  return { id, name: "Ada" };
}
 
type User = Awaited<ReturnType<typeof loadUser>>;
type _t1 = Expect<Equal<User, { id: number; name: string }>>;

Which one do I need?

I want...Use
All keys optional (PATCH body, defaults)Partial<T>
All keys requiredRequired<T>
All keys readonly (shallow)Readonly<T>
Only some keysPick<T, K>
All but some keysOmit<T, K> (or a StrictOmit / DistributiveOmit)
An object with known keys and one value typeRecord<K, V>
Remove members from a unionExclude<T, U>
Keep members of a union (e.g. one variant)Extract<T, U>
Drop null and undefinedNonNullable<T>
What a function returns / takesReturnType<typeof f> / Parameters<typeof f>
What a constructor takes / buildsConstructorParameters<C> / InstanceType<C>
What a promise resolves toAwaited<T>
Stop a parameter from widening a genericNoInfer<T>
Read or strip a this parameterThisParameterType<F> / OmitThisParameter<F>
Change case of a string literalUppercase, Lowercase, Capitalize, Uncapitalize

Recap

  • Utility types derive types from one source, so related types can't drift apart.
  • Partial, Required, Readonly change modifiers; all are shallow, and Readonly isn't enforced at runtime.
  • Pick checks its keys; Omit doesn't, and isn't distributive over unions. Use StrictOmit / DistributiveOmit when it matters.
  • Record<Union, V> requires every key; Record<string, V> lookups ignore missing keys unless noUncheckedIndexedAccess is on.
  • Exclude, Extract, NonNullable filter unions; they distribute member by member.
  • ReturnType, Parameters, ConstructorParameters, InstanceType read function and class types; pair with typeof; overloads resolve to the last signature.
  • Awaited unwraps promises recursively; NoInfer (5.4) blocks an inference site.
  • Uppercase, Lowercase, Capitalize, Uncapitalize are compiler intrinsics for string literals.

Interview cards

1 / 8