Phase 6 · Utility types in practice · Lesson 6.1
AdvancedThe 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 tooQuick 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"); // CChoosing 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 thereThe 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 onlyTwo things to remember:
- A union of literal keys makes every key required. That's a feature: add a new
Statusand the compiler lists every map you forgot to update. Wrap inPartialwhen you really want "some of them". Record<string, V>lies about lookups.cache["anything"]has typeV, notV | undefined, unless you enablenoUncheckedIndexedAccess. Prefer aMapor 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 onlyQuick 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: HttpClientInstanceType<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
loadUseris a value, andReturnTypeneeds a type. The error is 'loadUser' refers to a value, but is being used as a type here. Did you mean 'typeof loadUser'?- Even with
typeof, the return type of an async function is aPromise. Unwrap it withAwaited.
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 required | Required<T> |
| All keys readonly (shallow) | Readonly<T> |
| Only some keys | Pick<T, K> |
| All but some keys | Omit<T, K> (or a StrictOmit / DistributiveOmit) |
| An object with known keys and one value type | Record<K, V> |
| Remove members from a union | Exclude<T, U> |
| Keep members of a union (e.g. one variant) | Extract<T, U> |
Drop null and undefined | NonNullable<T> |
| What a function returns / takes | ReturnType<typeof f> / Parameters<typeof f> |
| What a constructor takes / builds | ConstructorParameters<C> / InstanceType<C> |
| What a promise resolves to | Awaited<T> |
| Stop a parameter from widening a generic | NoInfer<T> |
Read or strip a this parameter | ThisParameterType<F> / OmitThisParameter<F> |
| Change case of a string literal | Uppercase, Lowercase, Capitalize, Uncapitalize |
Recap
- Utility types derive types from one source, so related types can't drift apart.
Partial,Required,Readonlychange modifiers; all are shallow, andReadonlyisn't enforced at runtime.Pickchecks its keys;Omitdoesn't, and isn't distributive over unions. UseStrictOmit/DistributiveOmitwhen it matters.Record<Union, V>requires every key;Record<string, V>lookups ignore missing keys unlessnoUncheckedIndexedAccessis on.Exclude,Extract,NonNullablefilter unions; they distribute member by member.ReturnType,Parameters,ConstructorParameters,InstanceTyperead function and class types; pair withtypeof; overloads resolve to the last signature.Awaitedunwraps promises recursively;NoInfer(5.4) blocks an inference site.Uppercase,Lowercase,Capitalize,Uncapitalizeare compiler intrinsics for string literals.
Interview cards
1 / 8