Mastering TypeScript Mapped Types for Dynamic Object Transformations

Learn how to use TypeScript mapped types to create flexible and dynamic object transformations, ideal for enhancing type safety and code reusability in your projects.

If you're new to TypeScript and want to learn how to work with object types dynamically, mapped types are a fantastic feature to master. Mapped types allow you to create new types by transforming the properties of existing types. This tutorial will guide you through the basics of mapped types and help you understand how to apply them for practical object transformations.

### What are Mapped Types? Mapped types use TypeScript’s ability to iterate over keys of an existing type to create a new type with transformations applied to those keys. They are very useful when you want similar operations on multiple properties of an object without rewriting code.

Let's start with a simple example. Imagine you have an interface describing a user:

typescript
interface User {
  id: number;
  name: string;
  email: string;
}

If you want to create a new type where all `User` properties are optional, you could use a mapped type like this:

typescript
type PartialUser = {
  [K in keyof User]?: User[K];
};

The syntax `[K in keyof User]` means "for each key `K` in the keys of `User`". The `?` makes each property optional, and `User[K]` keeps the original property type.

You can now use `PartialUser` to create objects that may have any subset of the `User` properties.

typescript
const partialUser: PartialUser = {
  name: "Alice"
};

### More Practical Example: Readonly and Nullable Transformations Mapped types are also great for creating utility types like `Readonly` or `Nullable` that modify properties in common ways.

typescript
type ReadonlyUser = {
  readonly [K in keyof User]: User[K];
};

const readonlyUser: ReadonlyUser = {
  id: 1,
  name: "Bob",
  email: "bob@example.com"
};

// readonlyUser.id = 2; // Error: Cannot assign to 'id' because it is a read-only property.
typescript
type NullableUser = {
  [K in keyof User]: User[K] | null;
};

const nullableUser: NullableUser = {
  id: null,
  name: "Carol",
  email: null
};

### Adding Modifiers Dynamically You can combine mapped types with conditional types and modifiers to add even more dynamic transformations. For example, make all properties readonly except methods:

typescript
interface UserWithMethod {
  id: number;
  name: string;
  getEmail: () => string;
}

type ReadonlyExceptMethods<T> = {
  readonly [K in keyof T]: T[K] extends Function ? T[K] : Readonly<T[K]>;
};

const user: ReadonlyExceptMethods<UserWithMethod> = {
  id: 5,
  name: "Dan",
  getEmail: () => "dan@example.com"
};

// user.id = 6; // Error: Cannot assign to 'id' because it is a read-only property.
user.getEmail = () => "new@example.com"; // Allowed: methods are not readonly

### Conclusion Mapped types in TypeScript provide a flexible and powerful way to transform existing types into new versions, allowing you to write more expressive and reusable type-safe code. By mastering mapped types, you gain greater control over your types and can handle complex scenarios with ease.

Try experimenting with your own object types and transformations using mapped types to see how they can simplify your TypeScript development.