Mapped Types in Typescript
programmingtypescript
Published at Feb 22, 2023
I always admire the power of Type Systems in programming. Recently, TypeScript has fascinated me with its superpowers.
Mapped types enables to generate new types based on existing types. By defining a rule to transform each property of the original type, the resulting transformed properties collectively form the new type.
Toolbox
Here is the list of type mappers provided by Typescript out of the box:
// Make all properties of T optional: ? operator
// `keyof T` defines a set, we can iterate keys by [Property in keyof T]
type Partial<T> = {
  [Property in keyof T]?: T[Property];
};
type SomePartial = Partial<{ name: string; }> // { name?: string | undefined }

// Make all properties of T required: -? operator
type Required<T> = {
  [Property in keyof T]-?: T[Property];
};
type SomeRequired = Required<{ name?: string; }> // { name: string }

// Make all properties of T readonly: readonly operator
type Readonly<T> = {
  readonly [Property in keyof T]: T[Property];
};
type SomeReadonly = Readonly<{ name: string; }> // { readonly name: string }

// Composed
type SomeComposed = Partial<Readonly<{ name: string }>> // { readonly name?: string | undefined }
Bonus operator -readonly, maps types to writable ones. (not included in Typescript)
// Make all properties of T writable: -readonly operator
type Writable<T> = {
  -readonly [Property in keyof T]: T[Property];
};
type SomeWritable = Writable<{ readonly name: string; }> // { name: string }
Typescript 4.1 enables to use Key Remapping to mutate existing Type keys to create new one.
// As classic map method,  we can use `as operator` to map keys.
// [Property in (key of T -> as (map) -> NewProperties)]: Iterate mapped keys
type ReMapped<T> = {
  [Property in keyof T as NewProperties]: T[Property]
}

// Use template literal types to create new keys.
type Getter<T> = {
  [Property in keyof T as `get${Capitalize<string & Property>}`]: () => T[Property]
}
type SomeGetter = Getter<{ id: string }> // { getId: () => string }

// Filter out keys with Exclude
type ExcludeName<T> = {
  [Property in keyof T as Exclude<Property, 'name' | 'id'>]: T[Property]
}
type SomeNameExcluded = ExcludeName<{ name: string; age: number; id: string }> // { age: number }

// Pick keys with Extract
type ExtractName<T> = {
  [Property in keyof T as Extract<Property, 'name' | 'age'>]: T[Property]
}
type SomeNameExtracted = ExtractName<{ id: string, name: string; age: number }> // { name: string, age: number }
Moreover we can map types using Conditional Types, like ternary operator.
type StringIdentifiable<Type> = {
  [Property in keyof Type]: Type[Property] extends { id: string } ? true : false;
};

type SomeStringIdentifiable = StringIdentifiable<{
  user: { id: number, name: string, age: number },
  order: { id: string, amount: number }
}> // { user: false; order: true; }
Use Cases
1. Let's build a reducer state that stores entities in a normalized way.
// Define entities
type Entities = {
  essay: Essay // {id: string, title: string}
  user: User // {id: string, name: string}
}

// Instead of writing this:
type State = {
  essay: Record<Essay['id'], Essay>
  user: Record<User['id'], User>
}

// We can define `State` type by mapping `Entities`:
// Just add a new entity to `Entities` and it will be added to `State` automatically.
type State = {
  [Property in keyof Entities]: Record<Entities[Property]['id'], Entities[Property]>
}
2. To employ immutable design, Readonly<T> is useful for defining entities. However, in cases where a draft entity is necessary for mutation, the -readonly operator can be used to map a writable entity.
type Writable<T> = {
  -readonly [Property in keyof T]: T[Property]
}

type User = Readonly<{ id: string; name: string; }>
type WritableUser = Writable<User> // { id: string; name: string; }
The Point
Type definitions provide us a better comprehension of the codebase and allow for early error detection. Strongly typed codebase enhances code quality and scalability, promoting more efficient and secure team collaboration.
However, sometimes defining types can require a lot of repetitive work. Mapped Types are a very useful feature for reducing repetitiveness and keeping our code DRY. It also helps to have fewer sources of truth for our type definitions.