Mapped Types represent a declarative data structure transformation mechanism within TypeScript's type system. At the core of Mapped Types is iteration over a discrete finite set of string, numeric, and symbol key literals (K in keyof T), yielding a new object contract.
From the compiler's architectural perspective, Mapped Types fall into two fundamental categories:
[K in keyof T]. The compiler inspects the source type T deeply and automatically inherits its property metadata: immutability (readonly) and optionality (?) modifiers.[K in UnionKeys] or standard Record<K, V>. In these, the structural link to the original object is severed, and resulting properties are generated with default (mutable and required) flags.Beginning in TypeScript 4.1, the expressive power of Mapped Types expanded dramatically with Key Remapping via as.
Syntax:
type Mapped<T> = {
[K in keyof T as NewKeyType<K>]: TransformedValue<T[K]>;
};The fundamental rule of Key Remapping states: if the expression NewKeyType<K> resolves to never, that property is completely eliminated from the resulting interface. This enables clean, compile-time property filtering without intermediate synthetic objects.
The Key Remapping pipeline independently transforms property indices (keys) and property values:
+-------------------------------------------------------------------------+
| KEY REMAPPING MECHANICS VIA THE "as" CLAUSE |
| [K in keyof T as FilterOrRename<K>]: T[K] |
+-------------------------------------------------------------------------+
| |
| Source Object T: { id: string; secretToken: string; count: number } |
| |
| Step 1: Iterate over keyof T ("id" | "secretToken" | "count") |
| |
| Step 2: Compute the 'as' clause: |
| - "id" as `get${Capitalize<"id">}` => "getId" |
| - "secretToken" as never (Property eliminated!) => NEVER |
| - "count" as `get${Capitalize<"count">}` => "getCount" |
| |
| Step 3: Emit resulting contract: |
| { |
| getId: () => string; |
| getCount: () => number; |
| } |
+-------------------------------------------------------------------------+Modifier management is controlled with explicit addition (+) and subtraction (-) prefixes:
-readonly: Strips immutability, making fields mutable.
-?: Strips optionality, making all fields strictly required (equivalent to Required<T>).
Getters<State>).PickByValue<UserEntity, string | number>).snake_case DTOs into internal camelCase domain contracts at compile time.readonly modifiers when assembling domain entities inside Builder patterns.The keyof T operator extracts a union of string, numeric, and symbol literals from the source type.
For each key K, the remapping expression is calculated. If the result is never, the key is dropped.
Modifiers are adjusted: removing or adding readonly flags and questionToken (optionality).
For surviving keys, property values are evaluated and emitted into the final anonymous object type.
PickByValue & OmitByValue#Prior to TypeScript 4.1, filtering properties by value required fragile index-access workarounds. Key Remapping enables clean one-line declarations:
// Extract only properties whose values match ValueType
export type PickByValue<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};
// Exclude properties whose values match ValueType
export type OmitByValue<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? never : K]: T[K];
};
interface UserAccount {
id: string;
username: string;
age: number;
isActive: boolean;
loginCount: number;
permissions: readonly string[];
deleteAccount: () => Promise<void>;
}
// 1. Extract only string properties: { id: string; username: string }
export type StringProps = PickByValue<UserAccount, string>;
// 2. Extract only numeric properties: { age: number; loginCount: number }
export type NumericProps = PickByValue<UserAccount, number>;
// 3. Strip all methods, keeping data fields only (Serialized DTO):
export type DataOnly = OmitByValue<UserAccount, Function>;The as clause integrates directly with template literal string manipulations:
// Generate getters: transforms "name: string" into "getName: () => string"
export type CreateGetters<T> = {
[K in keyof T as K extends string ? `get${Capitalize<K>}` : never]: () => T[K];
};
// Generate setters: transforms "age: number" into "setAge: (val: number) => void"
export type CreateSetters<T> = {
[K in keyof T as K extends string ? `set${Capitalize<K>}` : never]: (value: T[K]) => void;
};
interface AppConfig {
apiUrl: string;
port: number;
debugMode: boolean;
}
export type AppConfigAccessors = CreateGetters<AppConfig> & CreateSetters<AppConfig>;
/*
Resulting AppConfigAccessors interface includes:
{
getApiUrl: () => string;
getPort: () => number;
getDebugMode: () => boolean;
setApiUrl: (value: string) => void;
setPort: (value: number) => void;
setDebugMode: (value: boolean) => void;
}
*/DeepMutable & DeepRequired#Standard library utilities Readonly<T> and Required<T> are strictly shallow. Recursive Mapped Types combining modifier stripping (-readonly and -?) yield deep mutators:
// Recursively strip readonly modifiers across all nested object levels
export type DeepMutable<T> = {
-readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepMutable<T[K]>
: T[K];
};
// Recursively strip optionality modifiers (?) across all nested levels
export type DeepRequired<T> = {
[K in keyof T]-?: T[K] extends object
? T[K] extends Function
? T[K]
: DeepRequired<T[K]>
: T[K];
};
interface ImmutableState {
readonly user: {
readonly profile: {
readonly bio?: string;
readonly socialLinks?: readonly string[];
};
};
}
// Convert structure into a completely mutable draft:
export type MutableDraft = DeepMutable<ImmutableState>;
// MutableDraft.user.profile.bio is now writable
// Convert structure into strictly required values (stripping undefined):
export type StrictState = DeepRequired<ImmutableState>;
// StrictState.user.profile.bio is now strictly 'string', rather than 'string | undefined'Below is an enterprise-grade entity transformer that sanitizes domain entities into client DTOs, stripping private fields and executable methods:
// Exclude private keys (prefixed with underscore) and secret tokens
type IsExposedKey<K> = K extends `_${string}`
? false
: K extends `${string}Password${string}`
? false
: K extends `${string}Secret${string}`
? false
: true;
// Production Public DTO Transformer
export type PublicDTO<TEntity> = {
-readonly [K in keyof TEntity as IsExposedKey<K> extends true
? TEntity[K] extends Function
? never
: K
: never]: TEntity[K] extends object
? TEntity[K] extends readonly unknown[]
? TEntity[K]
: PublicDTO<TEntity[K]>
: TEntity[K];
};
// Domain entity model
interface UserDomainModel {
id: string;
email: string;
_internalDatabaseClusterId: number;
hashedPasswordSalt: string;
billingSecretToken: string;
profile: {
displayName: string;
_cacheVersion: number;
};
saveToDatabase: () => Promise<void>;
validatePassword: (pwd: string) => boolean;
}
export type SafeUserDTO = PublicDTO<UserDomainModel>;
/*
The compiler cleanly emits the SafeUserDTO contract:
{
id: string;
email: string;
profile: {
displayName: string;
};
}
All methods, private fields (_), and secrets are cleanly purged!
*/A frequent pitfall is attempting to strip a property via T[K] extends V ? T[K] : never without an as clause. This does not remove the property; it leaves the key intact with a value of type never. You cannot assign to never, but the key persists in keyof, breaking Object.keys() iterations and runtime validators.
// ❌ BROKEN: leaves the key with a value of never ({ name: string, age: never })
type BadFilter<T> = {
[K in keyof T]: T[K] extends string ? T[K] : never;
};
// ✅ CORRECT: removes the key via 'as never' ({ name: string })
type GoodFilter<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};keyof T returns a union of string | number | symbol. Passing an unconstrained K to Capitalize<K> causes compiler error TS2344, as Capitalize requires a string argument. Constrain keys using K extends string or string & K.
// ❌ ERROR TS2344: Type 'K' does not satisfy the constraint 'string'
type BrokenGetters<T> = {
[K in keyof T as `get${Capitalize<K>}`]: () => T[K];
};
// ✅ CORRECT: safely narrow the key
type SafeGetters<T> = {
[K in keyof T as K extends string ? `get${Capitalize<K>}` : never]: () => T[K];
};Using Record<keyof T, Value> rather than homomorphic [K in keyof T] completely erases optionality metadata, forcefully converting all optional fields into required fields.
Recursive Mapped Types must not descend into host environment classes or circular structures (such as HTMLElement, Function, or Promise). Always provide terminal guard conditions for complex object types.
The compiler enumerates keys via keyof T, maintaining property index alignments.
For each key K, the filtering or string transformation expression is executed.
Any key evaluating to never is atomically eliminated from the property emission set.
Access modifiers are stripped (-?) or enforced (+readonly) across surviving fields.
An optimized object type literal is synthesized, ready for IDE completions and compiler checks.