Back to Blog
TypeScriptMapped TypesType ModifiersGenerics

TypeScript Mapped Type Modifiers

Master TypeScript mapped type modifiers for transforming object types with readonly and optional properties.

B
Bootspring Team
Engineering
April 17, 2019
8 min read

Mapped type modifiers allow you to add or remove readonly and optional modifiers when transforming types. Here's how to use them effectively.

Basic Mapped Types#

1// Transform all properties 2type Readonly<T> = { 3 readonly [P in keyof T]: T[P]; 4}; 5 6type Partial<T> = { 7 [P in keyof T]?: T[P]; 8}; 9 10// Usage 11interface User { 12 id: number; 13 name: string; 14 email: string; 15} 16 17type ReadonlyUser = Readonly<User>; 18// { readonly id: number; readonly name: string; readonly email: string; } 19 20type PartialUser = Partial<User>; 21// { id?: number; name?: string; email?: string; }

Adding Modifiers#

1// Add readonly with + 2type AddReadonly<T> = { 3 +readonly [P in keyof T]: T[P]; 4}; 5 6// Add optional with + 7type AddOptional<T> = { 8 [P in keyof T]+?: T[P]; 9}; 10 11// Both modifiers 12type ReadonlyPartial<T> = { 13 +readonly [P in keyof T]+?: T[P]; 14}; 15 16interface Config { 17 host: string; 18 port: number; 19} 20 21type ImmutableConfig = AddReadonly<Config>; 22// { readonly host: string; readonly port: number; }

Removing Modifiers#

1// Remove readonly with - 2type Mutable<T> = { 3 -readonly [P in keyof T]: T[P]; 4}; 5 6// Remove optional with - 7type Required<T> = { 8 [P in keyof T]-?: T[P]; 9}; 10 11// Remove both 12type MutableRequired<T> = { 13 -readonly [P in keyof T]-?: T[P]; 14}; 15 16interface PartialConfig { 17 readonly host?: string; 18 readonly port?: number; 19} 20 21type WritableConfig = Mutable<PartialConfig>; 22// { host?: string; port?: number; } 23 24type FullConfig = Required<PartialConfig>; 25// { readonly host: string; readonly port: number; } 26 27type EditableConfig = MutableRequired<PartialConfig>; 28// { host: string; port: number; }

Combining Modifiers#

1// Make all properties required and mutable 2type Concrete<T> = { 3 -readonly [P in keyof T]-?: T[P]; 4}; 5 6// Make all properties optional and readonly 7type Frozen<T> = { 8 +readonly [P in keyof T]+?: T[P]; 9}; 10 11interface Draft { 12 readonly title?: string; 13 readonly content?: string; 14 readonly tags?: string[]; 15} 16 17type Article = Concrete<Draft>; 18// { title: string; content: string; tags: string[]; } 19 20type Snapshot = Frozen<Article>; 21// { readonly title?: string; readonly content?: string; readonly tags?: string[]; }

Selective Modifiers#

1// Apply modifiers to specific keys 2type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; 3 4type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>; 5 6type ReadonlyBy<T, K extends keyof T> = Omit<T, K> & Readonly<Pick<T, K>>; 7 8interface User { 9 id: number; 10 name: string; 11 email?: string; 12 bio?: string; 13} 14 15type UserWithOptionalName = PartialBy<User, 'name'>; 16// { id: number; name?: string; email?: string; bio?: string; } 17 18type UserWithRequiredEmail = RequiredBy<User, 'email'>; 19// { id: number; name: string; email: string; bio?: string; } 20 21type UserWithReadonlyId = ReadonlyBy<User, 'id'>; 22// { readonly id: number; name: string; email?: string; bio?: string; }

Deep Modifiers#

1// Deep readonly 2type DeepReadonly<T> = { 3 readonly [P in keyof T]: T[P] extends object 4 ? T[P] extends Function 5 ? T[P] 6 : DeepReadonly<T[P]> 7 : T[P]; 8}; 9 10// Deep partial 11type DeepPartial<T> = { 12 [P in keyof T]?: T[P] extends object 13 ? T[P] extends Function 14 ? T[P] 15 : DeepPartial<T[P]> 16 : T[P]; 17}; 18 19// Deep required 20type DeepRequired<T> = { 21 [P in keyof T]-?: T[P] extends object 22 ? T[P] extends Function 23 ? T[P] 24 : DeepRequired<T[P]> 25 : T[P]; 26}; 27 28interface NestedConfig { 29 server: { 30 host: string; 31 port: number; 32 ssl?: { 33 cert: string; 34 key: string; 35 }; 36 }; 37 database?: { 38 url: string; 39 pool?: number; 40 }; 41} 42 43type ImmutableConfig = DeepReadonly<NestedConfig>; 44type DraftConfig = DeepPartial<NestedConfig>; 45type CompleteConfig = DeepRequired<NestedConfig>;

Conditional Modifiers#

1// Make properties optional based on type 2type OptionalByType<T, TargetType> = { 3 [P in keyof T as T[P] extends TargetType ? P : never]?: T[P]; 4} & { 5 [P in keyof T as T[P] extends TargetType ? never : P]: T[P]; 6}; 7 8interface Form { 9 id: number; 10 name: string; 11 description: string; 12 count: number; 13} 14 15type StringsOptional = OptionalByType<Form, string>; 16// { id: number; count: number; name?: string; description?: string; } 17 18// Make readonly based on naming convention 19type ReadonlyPrefixed<T> = { 20 [P in keyof T as P extends `readonly${string}` ? P : never]: T[P]; 21} & { 22 [P in keyof T as P extends `readonly${string}` ? never : P]: T[P]; 23};

Key Remapping with Modifiers#

1// Add prefix to keys 2type Prefixed<T, Prefix extends string> = { 3 [P in keyof T as `${Prefix}${string & P}`]: T[P]; 4}; 5 6// Getter/Setter pattern 7type Getters<T> = { 8 [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P]; 9}; 10 11type Setters<T> = { 12 [P in keyof T as `set${Capitalize<string & P>}`]: (value: T[P]) => void; 13}; 14 15interface State { 16 count: number; 17 name: string; 18} 19 20type StateGetters = Getters<State>; 21// { getCount: () => number; getName: () => string; } 22 23type StateSetters = Setters<State>; 24// { setCount: (value: number) => void; setName: (value: string) => void; } 25 26// Combined 27type StateAccessors = Getters<State> & Setters<State>;

Exclude Properties#

1// Remove properties by key 2type OmitByKey<T, K extends keyof T> = { 3 [P in keyof T as P extends K ? never : P]: T[P]; 4}; 5 6// Remove properties by type 7type OmitByType<T, U> = { 8 [P in keyof T as T[P] extends U ? never : P]: T[P]; 9}; 10 11// Keep only properties of type 12type PickByType<T, U> = { 13 [P in keyof T as T[P] extends U ? P : never]: T[P]; 14}; 15 16interface Mixed { 17 id: number; 18 name: string; 19 active: boolean; 20 count: number; 21 label: string; 22} 23 24type OnlyStrings = PickByType<Mixed, string>; 25// { name: string; label: string; } 26 27type NoStrings = OmitByType<Mixed, string>; 28// { id: number; active: boolean; count: number; }

Nullable Modifiers#

1// Make all properties nullable 2type Nullable<T> = { 3 [P in keyof T]: T[P] | null; 4}; 5 6// Make all properties non-nullable 7type NonNullableProps<T> = { 8 [P in keyof T]: NonNullable<T[P]>; 9}; 10 11// Optional and nullable 12type OptionalNullable<T> = { 13 [P in keyof T]?: T[P] | null; 14}; 15 16interface User { 17 id: number; 18 name: string; 19 email: string | null; 20} 21 22type NullableUser = Nullable<User>; 23// { id: number | null; name: string | null; email: string | null; } 24 25type RequiredUser = NonNullableProps<User>; 26// { id: number; name: string; email: string; }

Practical Examples#

1// Form state type 2type FormState<T> = { 3 values: T; 4 errors: Partial<Record<keyof T, string>>; 5 touched: Partial<Record<keyof T, boolean>>; 6 dirty: Partial<Record<keyof T, boolean>>; 7}; 8 9interface LoginForm { 10 email: string; 11 password: string; 12 remember: boolean; 13} 14 15type LoginFormState = FormState<LoginForm>; 16 17// API response wrapper 18type ApiResponse<T> = { 19 data: T; 20 meta: { 21 timestamp: number; 22 requestId: string; 23 }; 24}; 25 26type PartialApiResponse<T> = ApiResponse<DeepPartial<T>>; 27 28// Entity with timestamps 29type WithTimestamps<T> = T & { 30 readonly createdAt: Date; 31 readonly updatedAt: Date; 32}; 33 34type CreateInput<T> = Omit<T, 'id' | 'createdAt' | 'updatedAt'>; 35type UpdateInput<T> = Partial<CreateInput<T>>; 36 37interface Article { 38 id: number; 39 title: string; 40 content: string; 41 published: boolean; 42 createdAt: Date; 43 updatedAt: Date; 44} 45 46type CreateArticle = CreateInput<Article>; 47// { title: string; content: string; published: boolean; } 48 49type UpdateArticle = UpdateInput<Article>; 50// { title?: string; content?: string; published?: boolean; }

Utility Types Composition#

1// Combine utility types 2type Immutable<T> = DeepReadonly<Required<T>>; 3 4type Draft<T> = DeepPartial<Mutable<T>>; 5 6type Patch<T> = Partial<Mutable<T>>; 7 8// Apply multiple transformations 9type Transform<T> = { 10 -readonly [P in keyof T]-?: NonNullable<T[P]>; 11}; 12 13// Builder pattern types 14type Builder<T> = { 15 [P in keyof T as `with${Capitalize<string & P>}`]: (value: T[P]) => Builder<T>; 16} & { 17 build(): T; 18}; 19 20interface Config { 21 host: string; 22 port: number; 23 secure: boolean; 24} 25 26type ConfigBuilder = Builder<Config>; 27// { 28// withHost: (value: string) => Builder<Config>; 29// withPort: (value: number) => Builder<Config>; 30// withSecure: (value: boolean) => Builder<Config>; 31// build(): Config; 32// }

Best Practices#

Modifier Usage: ✓ Use + for clarity when adding ✓ Use - to remove existing modifiers ✓ Combine for complex transformations ✓ Create reusable utility types Common Patterns: ✓ Partial for optional updates ✓ Required for validation ✓ Readonly for immutability ✓ Mutable for editing Advanced Techniques: ✓ Deep modifiers for nested objects ✓ Conditional modifiers by type ✓ Key remapping with modifiers ✓ Selective property modification Avoid: ✗ Over-complex type transformations ✗ Deeply nested modifiers ✗ Ignoring function properties ✗ Circular type references

Conclusion#

Mapped type modifiers (+readonly, -readonly, +?, -?) provide powerful ways to transform object types. Use them to create utility types like Partial, Required, Readonly, and Mutable. Combine modifiers for complex transformations, apply them selectively to specific keys, and create deep versions for nested objects. These patterns enable type-safe APIs for forms, state management, and data transformation workflows.

Share this article

Help spread the word about Bootspring