TypeScript Mapped Types Can Rename, Filter, and Modify Keys
Build mapped types that preserve property details, change modifiers, remap names, and filter keys without changing runtime objects.
A mapped type iterates over a union of property keys and creates one property for each member. It can transform value types, add or remove readonly and optional modifiers, rename keys, or drop selected keys. The result exists only in the checker. It does not copy or reshape an object at runtime.
The basic form combines keyof with indexed access:
type Flags<Type> = {
[Key in keyof Type]: boolean;
};
type Features = {
search: () => void;
export: () => void;
};
type FeatureFlags = Flags<Features>;
// { search: boolean; export: boolean }
The mapped types handbook page defines this iteration model. Key takes each member of keyof Type, while Type[Key] can read the corresponding source property type.
A direct mapping preserves source modifiers
A mapping directly over keyof Type is homomorphic. TypeScript copies existing property modifiers before applying changes declared by the mapping.
type Boxed<Type> = {
[Key in keyof Type]: { value: Type[Key] };
};
type Source = {
readonly id: string;
label?: string;
};
type Result = Boxed<Source>;
// {
// readonly id: { value: string };
// label?: { value: string };
// }
The old TypeScript handbook’s description of homomorphic mapped types records why readonly and ? survive this form. A type such as Record<'id' | 'label', string> creates properties from an independent key union, so it has no source modifiers to preserve.
Homomorphic behavior also has a less obvious consequence for arrays, tuples, and primitive inputs. The compiler preserves their broader structure instead of always producing a plain object. Constrain utilities meant only for records:
type BoxedRecord<Type extends object> = {
[Key in keyof Type]: { value: Type[Key] };
};
The constraint rejects primitives, but arrays are objects too. If an API requires string-keyed records, state and test that narrower contract instead of assuming object excludes arrays.
Mapping modifiers changes mutability and presence
Mapped types accept readonly and ? modifiers. A - prefix removes a modifier, while + adds it. The plus sign is optional.
type MutableRequired<Type> = {
-readonly [Key in keyof Type]-?: Type[Key];
};
type Draft = {
readonly id: string;
title?: string;
};
type Saved = MutableRequired<Draft>;
// { id: string; title: string }
TypeScript 2.8 added removal with -readonly and -?, as recorded in the official mapped modifier release notes. These modifiers change static permissions. Removing readonly does not clone a frozen value or make a non-writable JavaScript property writable.
Optional properties deserve separate care. Under exactOptionalPropertyTypes, removing ? makes the property required but does not remove an explicitly written undefined from a union:
type Input = { value?: string | undefined };
type RequiredInput = { [Key in keyof Input]-?: Input[Key] };
// { value: string | undefined }
An as clause remaps names
TypeScript 4.1 added key remapping with as. Template literal types can derive new string keys while indexed access preserves each value relationship.
type Getters<Type> = {
[Key in keyof Type as
Key extends string ? `get${Capitalize<Key>}` : never
]: () => Type[Key];
};
type Model = {
id: number;
name: string;
};
type ModelGetters = Getters<Model>;
// { getId: () => number; getName: () => string }
The string guard matters because keyof may contain number or symbol, while Capitalize operates on string literal types. Intersecting with string, as in Capitalize<string & Key>, is a shorter alternative that drops non-string keys.
This utility still describes a type only. A value needs matching runtime code:
const getters: ModelGetters = {
getId: () => 42,
getName: () => 'Ada',
};
No mapped type creates those functions.
Mapping a key to never filters it out
The as expression can produce never for keys that should not appear. This filters names without routing through Omit:
type FunctionsOnly<Type> = {
[Key in keyof Type as
Type[Key] extends (...args: never[]) => unknown ? Key : never
]: Type[Key];
};
type Service = {
name: string;
start(): void;
stop(code: number): boolean;
};
type ServiceMethods = FunctionsOnly<Service>;
// { start(): void; stop(code: number): boolean }
This test is intentionally strict. An optional method has a property type that also includes undefined when read, so it will not match the function constraint as written. Use NonNullable<Type[Key]> if optional functions should count, and decide whether the output should remain optional.
Mapped types work best for one mechanical relationship between source properties and output properties. If the result needs several exceptions, recursive conditions, or runtime synchronization, a named interface and an explicit conversion function are usually easier to review. The key question is whether the output truly follows from the input type. If it does, a mapped type keeps that relationship in one place.