+ 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
+readonlyand+?. - We stripped modifiers using
-readonlyand-?. - We revealed the internal source code behind
Partial<T>,Required<T>, andReadonly<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!

