Master TypeScript's mapped types — how they iterate over keys to transform type shapes, modifier addition and removal, key remapping with as, and how every built-in utility type (Partial, Required, Readonly, Pick) is implemented with them.
Mapped types are TypeScript's for-loop for type definitions. Just as Array.map() transforms each element of an array into a new element, a mapped type iterates over each key in a type and transforms it into a new property — possibly changing the key name, the value type, or adding/removing modifiers like optional (?) and readonly. The result is a completely new type derived from the original, keeping the two in sync automatically: change the source type and every mapped type built from it updates without any manual edits.
// The fundamental pattern: [P in keyof T]: SomeType
// P is each key of T in turn, like a for..in loop over type keys
type Stringify<T> = {
[P in keyof T]: string; // every property becomes a string
};
interface User { id: number; name: string; active: boolean; }
type StringifiedUser = Stringify<User>;
// { id: string; name: string; active: string; }
// Preserve the original value type using T[P] (indexed access)
type Identity<T> = {
[P in keyof T]: T[P]; // copies the type exactly
};
// Identity<User> === User (structurally)
Mapped types can add or remove the readonly and ? (optional) modifiers using + (add) and - (remove) prefixes:
// Adding modifiers — + is implicit (same as no prefix)
type MyReadonly<T> = {
+readonly [P in keyof T]: T[P]; // add readonly to every property
};
type MyPartial<T> = {
[P in keyof T]+?: T[P]; // add ? to every property
};
// Removing modifiers — the - prefix
type MyRequired<T> = {
[P in keyof T]-?: T[P]; // -? removes the optional modifier
};
type MyMutable<T> = {
-readonly [P in keyof T]: T[P]; // -readonly removes readonly
};
// Combining: make every property required AND mutable
type Concrete<T> = {
-readonly [P in keyof T]-?: T[P];
};
TypeScript 4.1+ allows renaming keys in mapped types using an as clause — enabling prefix addition, case transformation, and key filtering:
// Add a 'get' prefix to every key
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface User { name: string; email: string; id: number; }
type UserGetters = Getters<User>;
// { getName: () => string; getEmail: () => string; getId: () => number; }
// Filter keys using never — never keys are omitted from the output
type OnlyStrings<T> = {
[P in keyof T as T[P] extends string ? P : never]: T[P];
};
type UserStringFields = OnlyStrings<User>;
// { name: string; email: string; } — id (number) is filtered out
// Map to event handler keys
type EventHandlers<T> = {
[P in keyof T as `on${Capitalize<string & P>}`]: (value: T[P]) => void;
};
Every built-in utility type is a mapped type. Understanding their implementations is the mark of a senior TypeScript developer:
// Partial — adds ?
type Partial<T> = { [P in keyof T]?: T[P]; };
// Required — removes ?
type Required<T> = { [P in keyof T]-?: T[P]; };
// Readonly — adds readonly
type Readonly<T> = { readonly [P in keyof T]: T[P]; };
// Pick — only iterates over the specified keys K
type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
// Record — K is any union of keys, V is the value type
type Record<K extends keyof any, V> = { [P in K]: V; };
// These are all in TypeScript's own lib.es5.d.ts — reading the source is instructive
Combining mapped types with conditional types enables powerful type transformations:
// Deep Partial — recursively makes all nested properties optional
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// Make only specific properties optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface User { id: number; name: string; email: string; }
type UserCreate = PartialBy<User, "id">;
// { id?: number; name: string; email: string; } — id optional, rest required
// Extract function-valued properties
type FunctionProperties<T> = {
[P in keyof T as T[P] extends Function ? P : never]: T[P];
};
// Flatten a nested type by one level
type Flatten<T> = {
[P in keyof T]: T[P] extends Array<infer U> ? U : T[P];
};
// When T is a union, the mapped type applies to each member separately
type NullableFields<T> = { [P in keyof T]: T[P] | null };
// Works correctly over unions via distributive keyof behaviour
type Config = { host: string; port: number };
type NullableConfig = NullableFields<Config>;
// { host: string | null; port: number | null; }Many developers think mapped types are only for making properties optional or readonly — those are just two applications. Mapped types can rename keys, filter keys, transform value types, add methods, create event handler maps, and generate entirely new type shapes from existing ones.
Many developers think you cannot filter out properties in a mapped type — mapping a key to never in an as clause removes it from the output type. This enables conditional key filtering in a single mapped type expression.
Many developers think Partial<T> makes nested objects optional recursively — it only applies to the top level. DeepPartial requires a recursive mapped type with a conditional type to handle nested objects.
Many developers think the -readonly and -? modifier syntax is specific to utility types and not available in user-defined mapped types — these modifiers work in any mapped type you write. They are part of the mapped type syntax, not special-cased for built-ins.
Many developers think key remapping (as clause) is only for adding prefixes — the as clause can produce any template literal string, filter keys to never, remap to a computed union, or even reverse-lookup a key from a value type.
Many developers treat mapped types as read-only reference material — they are a composable building block. Combining Pick, Omit, Partial, and custom mapped types with & intersections covers most real-world type transformation needs without conditional types.
Form validation schemas: mapped types generate a validation schema type from a data model — { [P in keyof FormData]: Validator<FormData[P]> } — keeping the schema and model in sync automatically when fields are added or removed.
API client generation: tools like openapi-typescript use mapped types to transform OpenAPI schemas into TypeScript types with correct optionality, readonly, and nullable flags derived from the schema definition.
ORM column mappers: TypeORM and Prisma generate mapped types for entity relations, partial update inputs, and select objects — all built with variations of Pick, Partial, and custom mapped types over the entity type.
React component prop transformation: Pick<ButtonProps, RequiredKeys> & Partial<Pick<ButtonProps, OptionalKeys>> is a common pattern for wrapper components that re-expose a strict subset of props with adjusted optionality.
Setter/getter generation: framework code that wraps store objects often uses Getters<State> and Setters<State> mapped types to generate typed accessor interfaces from a plain state shape.
Deep readonly for Redux state: Readonly<DeepReadonly<State>> using a recursive mapped type ensures immutability throughout the entire state tree, making accidental mutations compile errors.
What is the Mapped Type pattern in utility types — how are Partial, Readonly, and Record implemented?
Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.