At its core, a mapped type in TypeScript uses a property signature with a for...in
- like syntax to iterate over the properties of an existing type and create a new type. The basic syntax of a mapped type is as follows:
type MappedType<OriginalType> = {
[Property in keyof OriginalType]: NewPropertyType;
};
OriginalType
is the type whose properties we want to iterate over.Property
is a variable that represents each property name in the OriginalType
during the iteration.keyof OriginalType
is a type query that returns a union of all property names in the OriginalType
.NewPropertyType
is the type that we assign to each property in the new type.Let’s look at a simple example:
// Original type
type User = {
name: string;
age: number;
};
// Mapped type to make all properties optional
type OptionalUser = {
[Property in keyof User]?: User[Property];
};
const optionalUser: OptionalUser = {
name: 'John'
};
In this example, we create a new type OptionalUser
from the User
type. The OptionalUser
type has the same properties as User
, but all of them are optional.
We can use mapped types to make all properties in a type optional or readonly.
// Original type
type Person = {
firstName: string;
lastName: string;
age: number;
};
// Make all properties optional
type OptionalPerson = {
[Property in keyof Person]?: Person[Property];
};
// Make all properties readonly
type ReadonlyPerson = {
readonly [Property in keyof Person]: Person[Property];
};
const optionalPerson: OptionalPerson = {
firstName: 'Jane'
};
const readonlyPerson: ReadonlyPerson = {
firstName: 'Bob',
lastName: 'Smith',
age: 30
};
// This will cause a compilation error because the property is readonly
// readonlyPerson.age = 31;
We can also transform the types of properties in a type. For example, we can convert all string properties to numbers.
type StringToNumber<Type> = {
[Property in keyof Type]: Type[Property] extends string ? number : Type[Property];
};
type Example = {
name: string;
age: number;
};
type TransformedExample = StringToNumber<Example>;
const transformed: TransformedExample = {
name: 123,
age: 25
};
TypeScript already provides built - in utility types Partial<T>
and Readonly<T>
which are implemented using mapped types.
type Car = {
make: string;
model: string;
year: number;
};
// Using Partial<T>
type PartialCar = Partial<Car>;
const partialCar: PartialCar = {
make: 'Toyota'
};
// Using Readonly<T>
type ReadonlyCar = Readonly<Car>;
const readonlyCar: ReadonlyCar = {
make: 'Honda',
model: 'Civic',
year: 2022
};
We can use mapped types to filter properties based on their types.
type OnlyStringProperties<Type> = {
[Property in keyof Type as Type[Property] extends string ? Property : never]: Type[Property];
};
type MixedType = {
name: string;
age: number;
address: string;
};
type StringProperties = OnlyStringProperties<MixedType>;
const stringProps: StringProperties = {
name: 'Alice',
address: '123 Main St'
};
Mapped types can become complex quickly, especially when using conditional types and type queries. It’s important to keep the code simple and readable. If a mapped type becomes too complicated, consider breaking it down into smaller, more manageable types.
TypeScript provides several built - in utility types like Partial<T>
, Readonly<T>
, Pick<T, K>
, and Omit<T, K>
that are implemented using mapped types. Use these utility types whenever possible to avoid reinventing the wheel.
If you create custom mapped types, document them clearly. Explain what the type does, what the input and output types are, and any assumptions or limitations.
Mapped types in TypeScript are a powerful feature that allows developers to create new types by transforming existing types. They can be used to make properties optional or readonly, transform property types, and filter properties. By understanding the fundamental concepts, usage methods, common practices, and best practices of mapped types, developers can write more robust and maintainable TypeScript code.