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 : TExtract<T, U>—T extends U ? T : neverNonNullable<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, andReadonlycompute a data model’s derivatives from the original type; when the model grows, the derivatives grow with it.Omitdoes not constrain its second parameter withkeyof T; a nonexistent field name is silently ignored, whilePickgives theTS2344diagnostic.Exclude,Extract, andNonNullableare union filters built on the distribution behavior of conditional types.ReturnType,Parameters, andAwaitedderive types from existing signatures usinginfer; 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.