Mapped Types in TypeScript

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:

Map Object Type in TypeScript

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:

Mapped type in TypeScript

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:

TypeScript map Object Type

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:

Map TypeScript ObjectType using Quick Utility

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:

Map Object Type in TypeScript using Record Utility

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:

Advanced mapped types in Typescript

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:

Custom Mapped Types in TypeScript

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:

TypeScript Object Mapped Types

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:

Nested Mapped Object Types in TypeScript

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.

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.