More about Mapped Types
typescript mapped types
Mapped Types in TypeScript
Mapped types are a powerful feature in TypeScript that allow you to create new types by transforming the properties of existing types. They are especially useful for modifying the shape or behavior of types dynamically, enabling more flexible and reusable code.
1. Syntax of Mapped Types
The basic syntax for creating a mapped type is:
type NewType<T> = { [Key in keyof T]: TransformedType };
keyof T
: Extracts all keys from the typeT
as a union of string or symbol literal types.[Key in keyof T]
: Iterates over each key in the typeT
.T[Key]
: Accesses the type of the value corresponding to the keyKey
inT
.- Modifiers (
readonly
,?
): Used to adjust the properties (e.g., making themreadonly
or optional).
2. Examples of Mapped Types
2.1 Readonly Type
Transforms all properties of an object into readonly
properties, preventing them from being reassigned.
type Readonly<T> = { readonly [Key in keyof T]: T[Key] };
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = Readonly<Person>;
const person: ReadonlyPerson = { name: "Alice", age: 30 };
// person.name = "Bob"; // Error: cannot assign to 'name' because it is a read-only property.
2.2 Partial Type
Makes all properties of an object optional.
type Partial<T> = { [Key in keyof T]?: T[Key] };
interface Person {
name: string;
age: number;
}
type PartialPerson = Partial<Person>;
const person: PartialPerson = { name: "Alice" }; // `age` is optional
2.3 Required Type
Makes all properties of an object required.
type Required<T> = { [Key in keyof T]-?: T[Key] };
interface Person {
name?: string;
age?: number;
}
type RequiredPerson = Required<Person>;
const person: RequiredPerson = { name: "Alice", age: 30 }; // All properties are required
2.4 Record Type
Creates an object type where keys are specified and values have a uniform type.
type Record<K extends keyof any, T> = { [Key in K]: T };
type Roles = "admin" | "user" | "guest";
type Permissions = Record<Roles, string>;
const permissions: Permissions = {
admin: "all",
user: "read-only",
guest: "none",
};
2.5 Pick Type
Extracts a subset of properties from an object.
type Pick<T, K extends keyof T> = { [Key in K]: T[Key] };
interface Person {
name: string;
age: number;
address: string;
}
type NameAndAge = Pick<Person, "name" | "age">;
const person: NameAndAge = { name: "Alice", age: 30 }; // Only name and age are allowed
2.6 Omit Type
Removes specific properties from an object type.
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
interface Person {
name: string;
age: number;
address: string;
}
type WithoutAddress = Omit<Person, "address">;
const person: WithoutAddress = { name: "Alice", age: 30 }; // Address is omitted
3. Key Modifiers in Mapped Types
3.1 readonly
Modifier
Applies the readonly
modifier to all properties, making them immutable.
type Readonly<T> = { readonly [Key in keyof T]: T[Key] };
3.2 ?
Modifier
Applies the optional (?
) modifier to all properties.
type Partial<T> = { [Key in keyof T]?: T[Key] };
3.3 -?
Modifier
Removes the optional modifier from all properties.
type Required<T> = { [Key in keyof T]-?: T[Key] };
3.4 -readonly
Modifier
Removes the readonly
modifier from all properties.
type Mutable<T> = { -readonly [Key in keyof T]: T[Key] };
4. Combining Mapped Types with Utility Types
Mapped types can be combined with other TypeScript features like utility types, union types, and conditional types.
4.1 Combining with Union Types
You can use mapped types to iterate over a union of keys.
type OptionsFlags<T> = { [Key in keyof T]: boolean };
interface Features {
darkMode: () => void;
newUser: () => void;
}
type FeatureFlags = OptionsFlags<Features>;
const flags: FeatureFlags = { darkMode: true, newUser: false };
4.2 Using Conditional Types
You can conditionally modify the properties of a mapped type.
type Nullable<T> = { [Key in keyof T]: T[Key] | null };
interface User {
id: number;
name: string;
}
type NullableUser = Nullable<User>;
const user: NullableUser = { id: null, name: "Alice" }; // Properties can be null
5. Advanced Use Cases
5.1 Deep Readonly
Make all properties of an object (and its nested objects) readonly
.
type DeepReadonly<T> = {
readonly [Key in keyof T]: T[Key] extends object ? DeepReadonly<T[Key]> : T[Key];
};
interface NestedObject {
level1: {
level2: {
value: string;
};
};
}
const obj: DeepReadonly<NestedObject> = {
level1: {
level2: {
value: "Hello",
},
},
};
// obj.level1.level2.value = "New Value"; // Error
5.2 Filtering Keys
Extract keys from an object based on their types.
type FilterKeys<T, U> = { [Key in keyof T]: T[Key] extends U ? Key : never }[keyof T];
interface Example {
name: string;
age: number;
isActive: boolean;
}
type StringKeys = FilterKeys<Example, string>; // "name"
6. How Mapped Types Relate to Other Topics
Mapped types build upon foundational TypeScript concepts:
- Generics: Mapped types often use generics to create reusable type transformations.
- Type Inference: TypeScript infers types within mapped types to ensure transformations are type-safe.
- Union and Intersection Types: Mapped types frequently operate on unions or intersecting types to create flexible transformations.
Conclusion
Mapped types are a cornerstone of TypeScript’s type system, enabling developers to create dynamic, reusable, and type-safe transformations. By leveraging features like key modifiers, utility types, and conditional types, you can write more expressive and maintainable code tailored to your application’s needs. They provide the flexibility to adapt types without losing TypeScript’s static type-checking benefits.