While building a TypeScript app, I got an error caused by accidentally modifying data fetched from an API. To avoid such issues in the future, I used Readonly and other mapped types. It helped me to avoid the error by making parts of the data unchangeable.
In this TypeScript tutorial, I’ll explain mapped types in TypeScript and show you how mapped types like Readonly, Partial, and Record work using real-world examples.
Understanding TypeScript Mapped Types
TypeScript mapped types are a powerful feature that allows you to create new types by transforming properties of existing types. This can reduce code duplication and improve maintainability. Essentially, a mapped type iterates over the properties of an existing type and applies a transformation to each property.
Why Use Mapped Types in TypeScript?
Mapped types are particularly useful when you need to create variations of a type without manually redefining each property. For instance, if you have a type representing a user and you need a read-only version of that type, mapped types can automate this process.
Check out: Check if an Object is a String in TypeScript
Basic Syntax of Mapped Types
The basic syntax of a mapped type in TypeScript is as follows:
type MappedType<T> = {
[P in keyof T]: T[P];
};
type Sample = { name: string; age: number };
const sampleObj: MappedType<Sample> = { name: "John", age: 25 };
console.log("MappedType Sample:", sampleObj);Output:

Here, T is a generic type, P is a property key, and keyof T represents the keys of type T. This syntax creates a new type by mapping over each property of T.
Check out: TypeScript If-Else Conditional Logic
Example: Creating a Read-Only User Type
Let’s consider a real-world example. Suppose we have a User type representing users in a system:
type User = {
id: number;
name: string;
email: string;
address: string;
};To create a read-only version of this type, we can use a mapped type:
type ReadOnlyUser = {
readonly [P in keyof User]: User[P];
};
const user: ReadOnlyUser = {
id: 1,
name: "Alice",
email: "alice@example.com",
address: "123 Main St",
};
console.log("ReadOnlyUser:", user);Output:

Now, ReadOnlyUser is a type where all properties are read-only.
Check out: Optional Parameters in TypeScript Interfaces
Practical Applications of Mapped Types
Using Utility Types
TypeScript provides several built-in utility types that leverage mapped types, such as Partial, Readonly, Pick, and Record. These utilities simplify common transformations.
Partial
The Partial utility type makes all properties of a type optional. For example:
type PartialUser = Partial<User>;This is equivalent to:
type User = {
id: number;
name: string;
email: string;
address: string;
};Readonly
The Readonly utility type makes all properties read-only:
type ReadonlyUser = Readonly<User>;
const readonlyUser: ReadonlyUser = {
id: 2,
name: "Charlie",
email: "charlie@example.com",
address: "456 Broadway",
};Output:

Pick
The Pick utility type creates a new type by selecting a subset of properties from an existing type:
type UserContactInfo = Pick<User, 'name' | 'email'>;This results in:
const contactInfo: UserContactInfo = {
name: "Diana",
email: "diana@example.com",
};
console.log("UserContactInfo:", contactInfo);Output:

Record
The Record utility type constructs a type with a set of properties of a given type:
type UserRoles = 'admin' | 'editor' | 'viewer';
type UserRolePermissions = Record<UserRoles, string[]>;This creates a type where each role maps to an array of permissions:
const permissions: UserRolePermissions = {
admin: ["manage_users", "view_reports"],
editor: ["edit_content"],
viewer: ["read_only"],
};
console.log("UserRolePermissions:", permissions);Output:

Check out: Convert JSON to TypeScript Interface
Advanced Mapped Types
Conditional Types
Mapped types can be combined with conditional types for more complex transformations. For example, let’s create a type that makes properties nullable based on a condition:
type NullableProperties<T> = {
[P in keyof T]: T[P] | null;
};Using this with our User type:
type NullableUser = NullableProperties<User>;This results in:
const nullableUser: NullableUser = {
id: null,
name: "Eva",
email: null,
address: "789 Park Ave",
};
console.log("NullableUser:", nullableUser);Output:

Creating Custom Mapped Types
You can also create custom-mapped types to suit specific needs. For instance, let’s create a type that adds a prefix to all property names:
type PrefixProperties<T, Prefix extends string> = {
[P in keyof T as `${Prefix}${Capitalize<string & P>}`]: T[P];
};Using this with our User type:
type PrefixedUser = PrefixProperties<User, 'user'>;This results in:
const prefixedUser: PrefixedUser = {
userId: 3,
userName: "Frank",
userEmail: "frank@example.com",
userAddress: "101 Lake View",
};
console.log("PrefixedUser:", prefixedUser);Output:

Check out: Set Default Values for TypeScript Types
Real-World Example: E-Commerce Application
Let’s consider a more complex example relevant to an e-commerce application. Suppose we have a Product type:
type Product = {
id: number;
name: string;
description: string;
price: number;
category: string;
};Creating a Discounted Product Type
We want to create a type that includes discounted prices for products. We can use a mapped type to achieve this:
type DiscountedProduct = {
[P in keyof Product]: Product[P] extends number ? Product[P] | string : Product[P];
};This type will allow the price property to be either a number or a string (e.g., “50% off”).
Example Usage
const discountedProduct: DiscountedProduct = {
id: 101,
name: "Wireless Headphones",
description: "High-quality wireless headphones with noise cancellation",
price: "50% off",
category: "Electronics",
};
console.log("DiscountedProduct:", discountedProduct);Output:

Handling Complex Nested Types
Mapped types can also handle complex nested types. Suppose we have a Customer type with nested address information:
type Address = {
street: string;
city: string;
state: string;
zip: string;
};
type Customer = {
id: number;
name: string;
email: string;
address: Address;
};Check out: Check if an Object is Type of Interface in TypeScript
Making Nested Properties Optional
We can make all nested properties optional using a recursive mapped type:
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
type PartialCustomer = DeepPartial<Customer>;This results in:
type PartialCustomer = {
id?: number;
name?: string;
email?: string;
address?: {
street?: string;
city?: string;
state?: string;
zip?: string;
};
};Example Usage
const partialCustomer: PartialCustomer = {
id: 202,
address: {
city: "San Francisco",
},
};
console.log("PartialCustomer:", partialCustomer);Output:

Conclusion
In this TypeScript tutorial, we have learned how mapped types in TypeScript, like Readonly, Partial, and Record, can help us write safer and maintainable code. We also discussed the practical examples to understand how these types work in real projects and how they can prevent common mistakes like accidental data changes.

I am Bijay Kumar, a Microsoft MVP in SharePoint. Apart from SharePoint, I started working on Python, Machine learning, and artificial intelligence for the last 5 years. During this time I got expertise in various Python libraries also like Tkinter, Pandas, NumPy, Turtle, Django, Matplotlib, Tensorflow, Scipy, Scikit-Learn, etc… for various clients in the United States, Canada, the United Kingdom, Australia, New Zealand, etc. Check out my profile.