How to create mapped types in TypeScript

Mapped types in TypeScript allow you to transform an existing type into a new type by mapping over its properties. This can be useful for creating new types that are based on existing types, but with some modifications.

They can also help avoid repeating yourself and make your types more flexible and reusable. Some use cases for mapped types are:

  • Creating partial, required, or readonly versions of existing types.
  • Creating new types with different property names based on existing ones.
  • Creating new types with different property values based on existing ones.

You can also use features like template literal types, conditional types, and generics to create more complex mapped types.

Built-in mapped types

TypeScript provides some global utility types that are based on mapped types and can be used to manipulate existing types:

  • Partial<T>: Makes all properties of T optional.
  • Required<T>: Makes all properties of T required.
  • Readonly<T>: Makes all properties of T readonly.
  • Record<K, T>: Creates a type with keys of type K and values of type T.
  • Pick<T, K>: Creates a type that picks a set of properties K from T.
  • Omit<T, K>: Creates a type that omits a set of properties K from T.
  • Exclude<T, U>: Creates a type that excludes all types that are assignable to U from T.
  • Extract<T, U>: Creates a type that extracts all types that are assignable to U from T.
  • NonNullable<T>: Creates a type that excludes null and undefined from T.

Here are some examples of built-in mapped types:

type Person = {
  name: string;
  age: number;
};

type MaybePerson = Partial<Person>;
// Equivalent to:
// type MaybePerson = {
//   name?: string;
//   age?: number;
// };

type Animal = {
  name: string;
  species: string;
  age: number;
};

type Pet = Pick<Animal, "name" | "age">;
// Equivalent to:
// type Pet = {
//   name: string;
//   age: number;
// };

How to create mapped types

Index signatures are a way to declare the types of properties that have not been declared ahead of time.

type IndexType = {
  [key: KeyType]: ValueType;
};

For example, here is an index type that declares an object that can have any string keys and any values:

type AnyObject = {
  [key: string]: any;
};

let obj: AnyObject = {
  foo: 1,
  bar: "hello",
  baz: true,
};

Mapped types build on the syntax for index signatures by adding a way to iterate over the keys of another type and transform them into new keys and values. They use the following syntax:

type MappedType<Type> = {
  [Property in keyof Type as NewProperty]: NewType;
};

Here, Property is a type variable that iterates over the keys of Type. NewProperty is an expression that defines how to create new keys based on Property. NewType is an expression that defines how to create new values based on Type and Property.

  1. They can use modifiers like readonly and ? to add or remove them from the properties. For example, here is a mapped type that takes a generic type Type and creates a new type ReadonlyOptionalType that has the same properties as Type but with readonly and optional modifiers:
type ReadonlyOptionalType<Type> = {
  readonly [Property in keyof Type]?: Type[Property];
};

type Book = {
  title: string;
  author: string;
  pages: number;
};

type BookInfo = ReadonlyOptionalType<Book>;
// Equivalent to:
// type BookInfo = {
//   readonly title?: string;
//   readonly author?: string;
//   readonly pages?: number;
// };
  1. They can also use key remapping with the as keyword to change the names of the properties. For example, here is a mapped type that takes a generic type Type and creates a new type SuffixType that has the same properties as Type but with keys suffixed by “_suffix”:
type SuffixType<Type> = {
  [Property in keyof Type as `${Property & string}_suffix`]: Type[Property];
};

type Animal = {
  name: string;
  species: string;
  age: number;
};

type AnimalSuffix = SuffixType<Animal>;
// Equivalent to:
// type AnimalSuffix = {
//   name_suffix: string;
//   species_suffix: string;
//   age_suffix: number;
// };
  1. They can also use conditional types to filter out some properties based on some condition. For example, here is a mapped type that takes a generic type Type and creates a new type NumberType that has only the properties of Type whose values are numbers:
type NumberType<Type> = {
  [Property in keyof Type]: Type[Property] extends number ? Type[Property] : never;
};

type Person = {
  name: string;
  age: number;
  height: number;
};

type PersonNumbers = NumberType<Person>;
// Equivalent to:
// type PersonNumbers = {
//   name: never;
//   age: number;
//   height: number;
// };
  1. Mapped types with generics are useful for creating types that are more flexible and reusable by allowing other types to be passed as arguments. They can also help avoid repeating yourself and make your types more expressive and descriptive.
// TypeScript v4.5.5

type ReplaceType<Type, Value> = {
  [Property in keyof Type]: Value;
};

type Book = {
  title: string;
  author: string;
  pages: number;
};

type BookStatus = ReplaceType<Book, boolean>;
// Equivalent to:
// type BookStatus = {
//   title: boolean;
//   author: boolean;
//   pages: boolean;
// };

type PrefixType<Type, Prefix extends string> = {
  [Property in keyof Type as `${Prefix}${Property & string}`]: Type[Property];
};

type User = {
  name: string;
  email: string;
  password: string;
};

type UserForm = PrefixType<User, "form_">;
// Equivalent to:
// type UserForm = {
//   form_name: string;
//   form_email: string;
//   form_password: string;
// };