Skip to content
academia.sh

Lesson 17 / 23

Utility Types

The standard library's ready-made type transforms; field modifiers, picking and omitting fields, building a table, filtering unions, and tools that derive types from signatures.

Contents

Most of the transforms written by hand in this topic — a read-only version, an optional version, filtering a member out of a union — are already available in the standard library. These are called utility types.

All of them are written with the mechanisms built in earlier lessons: mapped types, conditional types, keyof, and infer. This lesson covers what each one does, how it is built, and how it is used to write the derivatives of the measurement model.

Field Modifiers and Field Selection

The four most commonly used transforms produce four derivatives of the measurement model in a single line:

interface Measurement {
  id: string;
  sensor: string;
  value: number;
  unit: "C" | "Pa" | "%";
  time: number;
}

type MeasurementSummary = Pick<Measurement, "id" | "value" | "unit">;
type MeasurementInput = Omit<Measurement, "id" | "time">;
type MeasurementPatch = Partial<Omit<Measurement, "id">>;
type ArchivedRecord = Readonly<Measurement>;

const summary: MeasurementSummary = { id: "s-01", value: 21.4, unit: "C" };
const input: MeasurementInput = { sensor: "temperature", value: 21.4, unit: "C" };
const patch: MeasurementPatch = { value: 22.1 };
const archived: ArchivedRecord = {
  id: "s-01",
  sensor: "temperature",
  value: 21.4,
  unit: "C",
  time: 1706000000000,
};

console.log(summary.id, input.sensor, patch.value, archived.time);

The output is s-01 temperature 22.1 1706000000000.

The four types mean the following. MeasurementSummary carries the three fields a listing screen needs. MeasurementInput is the fields given when creating a new record — the id and timestamp are produced by the system. MeasurementPatch is a partial update request; every field except id can be given, and none is required. ArchivedRecord is a record that cannot be changed.

Because all four are computed from the original type, the corresponding derivatives grow on their own when a new field is added to Measurement. Had they been written by hand, this link would have broken.

Two diagnostics show that the constraints are really enforced:

interface Measurement {
  id: string;
  value: number;
  time: number;
}

type MeasurementInput = Omit<Measurement, "id">;
type ArchivedRecord = Readonly<Measurement>;

const input: MeasurementInput = { id: "s-01", value: 21.4, time: 1 };

declare const archived: ArchivedRecord;
archived.value = 22.1;
r2.ts(10,35): error TS2353: Object literal may only specify known properties, and 'id' does not exist in type 'MeasurementInput'.
r2.ts(13,10): error TS2540: Cannot assign to 'value' because it is a read-only property.

The definitions of these four types are the same ones written by hand in the mapped types lesson:

Utility type Equivalent
Partial<T> { [A in keyof T]?: T[A] }
Required<T> { [A in keyof T]-?: T[A] }
Readonly<T> { readonly [A in keyof T]: T[A] }
Pick<T, A> { [B in A]: T[B] }
Record<A, D> { [B in A]: D }

Omit is not in this list, because it is built not directly by mapping but by a composition: Pick<T, Exclude<keyof T, A>>. The names to exclude are first removed from the keyof T union, and the remaining names are picked.

The Checking Difference Between Omit and Pick

This composition has a result that is easy to miss:

interface Measurement {
  id: string;
  value: number;
}

type A = Omit<Measurement, "missingField">;
type B = Pick<Measurement, "missingField">;
r3.ts(7,28): error TS2344: Type '"missingField"' does not satisfy the constraint 'keyof Measurement'.

The diagnostic comes only for Pick. Omit’s second parameter is constrained not by keyof T but by any property key; a nonexistent field name is silently ignored, and the result is the same as Measurement.

The maintenance cost of this is: when a field of Measurement is renamed, the Omit notations that exclude it silently become ineffective, and the field that should have been excluded comes back. In Pick notations, the compiler gives a diagnostic.

Practical consequence: when the field set is small, Pick is preferred. If Omit is needed, a wrapper that tightens the constraint can be written:

interface Measurement {
  id: string;
  value: number;
}

type Without<T, A extends keyof T> = Omit<T, A>;

type Safe = Without<Measurement, "id">;
const value: Safe = { value: 21.4 };
console.log(value.value);

The output is 21.4. If the line type Wrong = Without<Measurement, "missingField">; is added at the end of the file, after a blank line:

r7.ts(12,35): error TS2344: Type '"missingField"' does not satisfy the constraint 'keyof Measurement'.

The only difference is putting the extends keyof T constraint on the type parameter; the body still uses Omit.

Union Filters

Three utility types are built on the distribution behavior of conditional types:

type Unit = "C" | "Pa" | "%";
type Interval = [min: number, max: number];

type Intervals = Record<Unit, Interval>;
type NumericUnit = Exclude<Unit, "%">;
type PercentUnit = Extract<Unit, "%" | "ppm">;
type DefinedValue = NonNullable<number | null | undefined>;

const intervals: Intervals = {
  C: [-40, 85],
  Pa: [0, 200000],
  "%": [0, 100],
};
const numeric: NumericUnit = "Pa";
const percent: PercentUnit = "%";
const value: DefinedValue = 21.4;

console.log(intervals.C[1], numeric, percent, value);

The output is 85 Pa % 21.4.

Their definitions are one line with conditional types:

  • Exclude<T, U>T extends U ? never : T
  • Extract<T, U>T extends U ? T : never
  • NonNullable<T>T & {}

In the Extract example, the value "ppm" is not in the Unit union and does not enter the result; it can be thought of as an intersection. NonNullable intersects with the empty object type to filter out the null and undefined members.

Deriving Types from Signatures

The last group is built on infer and derives a type from an existing function’s signature:

interface Measurement {
  id: string;
  value: number;
}

function createMeasurement(id: string, value: number) {
  return { id, value, createdAt: 0 };
}

async function fetchMeasurement(id: string): Promise<Measurement> {
  return { id, value: 21.4 };
}

type CreatedRecord = ReturnType<typeof createMeasurement>;
type CreationArguments = Parameters<typeof createMeasurement>;
type FetchedRecord = Awaited<ReturnType<typeof fetchMeasurement>>;

const record: CreatedRecord = { id: "s-01", value: 21.4, createdAt: 0 };
const args: CreationArguments = ["s-02", 22.1];
const fetched: FetchedRecord = { id: "s-03", value: 23.0 };

console.log(record.createdAt, createMeasurement(...args).id, fetched.value);

The output is 0 s-02 23. If the line const wrong: CreationArguments = ["s-04"]; is added at the end of the file, after a blank line:

r5.ts(24,7): error TS2322: Type '[string]' is not assignable to type '[id: string, value: number]'.
  Source has 1 element(s) but target requires 2.

The diagnostic shows that Parameters’s result is a named tuple. ReturnType gives the inferred return type of the createMeasurement function; Awaited takes the type of the value a promise produces when resolved, introduced in the Asynchronous JavaScript course, and also unwraps nested promises.

This group’s typical use is using a function’s return type elsewhere without writing it by hand. One warning applies: this link is one-directional, and when the function’s body changes, the derived type changes silently along with it. The rule established in the Type Inference lesson applies here — the type of a boundary exposed outward should be written, not left to inference.

Choosing a Utility Type

Need Utility type
Make all fields optional Partial<T>
Make all fields required Required<T>
Make all fields read-only Readonly<T>
Keep specific fields Pick<T, A>
Remove specific fields Omit<T, A>
Build a table from a key set Record<A, D>
Remove a member from a union Exclude<T, U>
Select a member from a union Extract<T, U>
Filter out empty values NonNullable<T>
Derive a type from a signature ReturnType, Parameters, Awaited

Every row in the table rests on a mechanism built in this topic. There is no need to memorize the utility types; once what they do and how they are built is known, they are remembered on their own, and a new one can be written when a transform not on the list is needed.

Summary

  • Utility types are the standard library’s ready-made forms of the mapped and conditional type mechanisms built in earlier lessons.
  • Pick, Omit, Partial, and Readonly compute a data model’s derivatives from the original type; when the model grows, the derivatives grow with it.
  • Omit does not constrain its second parameter with keyof T; a nonexistent field name is silently ignored, while Pick gives the TS2344 diagnostic.
  • Exclude, Extract, and NonNullable are union filters built on the distribution behavior of conditional types.
  • ReturnType, Parameters, and Awaited derive types from existing signatures using infer; the derived type changes silently when the body changes.

Next Step

Measurement records do not always carry a flat structure: a record can have sub-records underneath it, a configuration object can have nested sections underneath it. Transforms applied to the depth of such structures — making every level read-only, counting nested key paths — require a type to refer to itself. The next lesson covers recursive types and the limits the compiler places on them.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close