A mapped type builds an object type by looping over keys: { [K in keyof T]: NewType } visits each property of T the way for...in visits an object, with T[K] as the original property type. The modifiers readonly and ? can be added with + (the default) or removed with -. Partial, Required and Readonly are one-line mapped types; add a conditional type and recursion and you can make a whole configuration tree immutable:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
type Flags<T> = { [K in keyof T]-?: boolean }; // -? removes optionality
type AppConfig = { api: { baseUrl: string; retries: number }; hosts: string[]; debug?: boolean };
const config: DeepReadonly<AppConfig> = { api: { baseUrl: "/api", retries: 3 }, hosts: [] };
config.api.retries = 5;
config.hosts.push("b.example.com");
const shown: Flags<AppConfig> = { api: true, hosts: false };config.ts(7,12): error TS2540: Cannot assign to 'retries' because it is a read-only property.
config.ts(8,14): error TS2339: Property 'push' does not exist on type 'readonly string[]'.
config.ts(9,7): error TS2741: Property 'debug' is missing in type '{ api: true; hosts: false;
}' but required in type 'Flags<AppConfig>'.Built-in Readonly<AppConfig> would block only config.api = ...; the recursion reaches retries, and mapping over an array type yields readonly string[].
Since TypeScript 4.1 an as clause renames or drops keys while mapping, and a key mapped to never disappears: { [K in keyof T as K extends "password" ? never : K]: T[K] } strips a secret from a response type.