Mapped Modifiers

The Course
•
5 min read

Intermediate introduced a readonly mapped type. Now read the modifiers explicitly: -readonly removes a readonly permission restriction, and -? removes an optional-property marker. Neither expression creates or modifies a JavaScript object.

For a configuration type with optional fields, RequiredFields<T> can require callers to supply all fields. The runtime implementation still needs defaults or validation if data arrives incomplete. A declaration cannot fill missing values in JSON.

Do not confuse making a writable view with obtaining independent ownership. Two variables can still refer to the same object. Use a new object when a transformation promises a copy. Prefer built-in Required, Readonly and Partial in ordinary application code; these small definitions teach you to read the underlying syntax.

Keep transformations shallow. If a field contains a nested record, removing readonly at the outer level does not rewrite that nested record’s type. Test the exact caller behavior your API needs instead of assuming every modifier propagates recursively.

Predict, check, and run

Write the output before running. Use Check types first, then Check & run. Types disappear at runtime: explain both what the compiler checks and what the code actually does. Each editor is an independent ES module; await asynchronous work explicitly.

Predict, then runTypeScript
type RequiredFields<T> = { [K in keyof T]-?: T[K] };

type Writable<T> = { -readonly [K in keyof T]: T[K] };

type Options = {
  readonly title?: string;

  readonly limit?: number;
};

const options: Writable<RequiredFields<Options>> = { title: "Types", limit: 2 };

options.limit = 3;
console.log(options.title, options.limit);

Enable JavaScript to edit and type-check this example.

Check your prediction
Types 3

Read the compiler error

Predict the rejected relationship before checking. Read the real diagnostic and its source location; fix the contract or the value instead of hiding it with an assertion.

Find and fix the type errorTypeScript
type Complete<T> = { [K in keyof T]-?: T[K] };

const settings: Complete<{
  label?: string;

  enabled?: boolean;
}> = {
  label: "Types"
};

console.log(settings);

Enable JavaScript to edit and type-check this example.

Check your diagnosis

Removing ? requires enabled to be present; no runtime default is inserted.

Permissions and required fields change independently

Removing optional markers changes what a caller must supply; removing readonly changes which assignments are allowed through a view. Neither fills missing runtime data or allocates another record. Compose those transformations only when both changes fit the public API.

An explicit undefined union is another choice. Required does not mean nonblank or positive, and writable does not mean independently owned. Test the exact caller operation you want to accept or reject rather than treating these modifiers as a general validation tool.

Permissions and required fields change independently: traceTypeScript
type Complete<T> = { [K in keyof T]-?: T[K] };

type Draft = {
  enabled?: boolean;

  minutes?: number;
};

const value: Complete<Draft> = { enabled: false, minutes: 0 };

console.log(value.enabled, value.minutes);

Enable JavaScript to edit and type-check this example.

Check your prediction
false 0

Completeness requires both fields, while false and zero remain legitimate values. Optional markers alone said nothing about their domain meaning.

Permissions and required fields change independently: contrastTypeScript
type Writable<T> = { -readonly [K in keyof T]: T[K] };

const source: { readonly minutes: number } = { minutes: 20 };
const view: Writable<typeof source> = source;

view.minutes = 30;
console.log(source.minutes);
console.log(view === source);

Enable JavaScript to edit and type-check this example.

Check your prediction
30
true

The writable view shares the original record. Removing an assignment restriction did not create a copy; a library must avoid advertising this as an isolated mutable draft.

Try the rule on new inputs

Compare Required, Readonly and a new spread record. Name which operation changes checking permissions, which requires fields and which creates new runtime identity. Test a nested field separately.

Build it yourself

Exercise

Mapped Modifiers: practise

Create Complete<T> removing optional markers and ReadonlyFields<T> adding readonly. Make a complete readonly settings record with label Types and enabled true. Print Types true; verify deleting enabled is a type error.

Your workspaceTypeScript
// Start from the stated contract. Add a valid call and an edge case.

Enable JavaScript to edit and type-check this example.

Hint 1: the idea

Compose two small type transformations rather than pretending one clones data.

Hint 2: a first step

The optional modifier belongs after the closing key bracket; readonly belongs before it.

Show one solution
One possible solutionTypeScript
type Complete<T> = { [K in keyof T]-?: T[K] };

type ReadonlyFields<T> = { readonly [K in keyof T]: T[K] };

type Draft = {
  label?: string;

  enabled?: boolean;
};

const settings: ReadonlyFields<Complete<Draft>> = {
  label: "Types",
  enabled: true
};

console.log(settings.label, settings.enabled);

Enable JavaScript to edit and type-check this example.

Types true

Trace the implementation against the task before comparing styles. A passing example supports that case; use the changes below to challenge the wider contract.

Change it and test it

Try a missing enabled property and a nested mutable array. Explain the separate effects of completeness, readonly and copying. Compare with Readonly<Required<Draft>>.

Was this article helpful?

Your response is saved on this device.