Skip to main content

TS Ep 39: Mapped Type Modifiers (`+`, `-`, `?`, `readonly`)

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 39: This Article
Mapped Types do not merely transform property values; they can fundamentally alter the structural modifiers of an object shape. By applying the + or - prefixes to readonly or ?, you can lock down mutable objects or forcefully unlock strict constraints.

1. Modifier Preservation (Homomorphic Mapping)
#

When you map over an object using the keyof operator without specifying any modifiers, TypeScript performs a Homomorphic Mapping. This means the compiler automatically preserves all existing readonly and ? modifiers from the source object!

interface MixedState {
  readonly id: string; // Readonly
  username: string;    // Standard
  age?: number;        // Optional
}

// 🟢 Standard mapping preserves all modifiers automatically!
type AsyncState = {
  [K in keyof MixedState]: Promise<MixedState[K]>;
};

/* Inferred Type:
{
  readonly id: Promise<string>;
  username: Promise<string>;
  age?: Promise<number>;
}
*/

2. Adding Modifiers (+readonly, +?)
#

You can explicitly inject new modifiers into the mapped output by placing readonly before the bracket, or ? after the bracket.

(Note: Specifying readonly or ? is syntactic sugar for +readonly and +?).

interface MutableUser {
  id: string;
  name: string;
}

// 🟢 Adds both 'readonly' and '?' to every property!
type LockedOptionalUser = {
  readonly [K in keyof MutableUser]?: MutableUser[K];
};

/* Inferred Type:
{
  readonly id?: string;
  readonly name?: string;
}
*/

The Secret of Built-in Utilities
#

This exact syntax is how the global Partial<T> and Readonly<T> utilities are implemented under the hood in the TypeScript compiler:

// The actual TypeScript implementation of Partial<T>
type Partial<T> = {
  [P in keyof T]?: T[P]; // Adds the '?' modifier
};

// The actual TypeScript implementation of Readonly<T>
type Readonly<T> = {
  readonly [P in keyof T]: T[P]; // Adds the 'readonly' modifier
};

3. Removing Modifiers (-readonly, -?)
#

What if you receive a highly restricted, locked-down object with optional fields, and you need to build a mutable, fully-resolved version?

You can forcefully strip modifiers away by prepending them with a minus sign (-).

interface LockedConfig {
  readonly host?: string;
  readonly port?: number;
}

// 🟢 '-readonly' strips immutability.
// 🟢 '-?' strips optionality (forcing fields to be required).
type MutableRequiredConfig = {
  -readonly [K in keyof LockedConfig]-?: LockedConfig[K];
};

/* Inferred Type:
{
  host: string;
  port: number;
}
*/

The Secret of Required<T>
#

This is exactly how TypeScript’s global Required<T> utility type operates under the hood:

// The actual TypeScript implementation of Required<T>
type Required<T> = {
  [P in keyof T]-?: T[P]; // Strips the '?' modifier
};

4. Custom Utility: Mutable<T>
#

While TypeScript provides Readonly<T>, it does not provide a built-in Mutable<T> utility to reverse it. You can build your own effortlessly using mapping modifiers:

// 🟢 Custom Utility: Strips readonly from all properties!
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

interface FrozenData {
  readonly apiKey: string;
  readonly env: string;
}

// Result is completely mutable:
const data: Mutable<FrozenData> = {
  apiKey: "secret",
  env: "production",
};

// 🟢 Reassignment is now permitted!
data.env = "staging"; 

Summary & Next Steps
#

In this episode:

  • We demonstrated Homomorphic Mapping, where TypeScript automatically preserves existing source modifiers.
  • We added modifiers using +readonly and +?.
  • We stripped modifiers using -readonly and -?.
  • We revealed the internal source code behind Partial<T>, Required<T>, and Readonly<T>.
  • We constructed a custom Mutable<T> utility using -readonly.

In Episode 40: Key Remapping with as, we conclude the Intermediate module by learning how to rename object keys dynamically (like changing id to setId) during a mapped type loop!

typescript - This article is part of a series.
Part 39: This Article