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.