Chapters

  • 0. Introduction

    Free

  • 1. Types & Values

    Free

  • 2. Types are just data

    Free

  • 3. Objects & Records

    Free

  • 4. Arrays & Tuples

    Free

  • 5. Conditional Types
  • 6. Loops with Recursive Types
  • 7. Template Literal Types
  • 8. The Union Type Multiverse
  • 9. Loops with Mapped Types
  • 10. Assignability In Depth

    wip

  • 11. Debugging Types

    wip

Articles

Subscribe

About

Made with ❤️ by @GabrielVergnaud

Loops with Mapped Types

No doubt about it, transforming objects {} is at the core of what we do as developers. Building awesome features usually requires getting JSON objects from external APIs into our application and turning them into something that makes sense for our users. In the process, we tend to create a lot of slightly different copies of our data. Wouldn't it be nice if we didn't have to type them manually?

Here is the good news — we can leverage TypeScript's awesome type inference to transform our object types using a super powerful feature called Mapped Types.

In this chapter, we will learn how to transform and filter object types using Mapped Types, and combine them with other features of the type system such as Template Literal Types and Conditional Types to build functions with super smart type inference.

For example, we will learn how to type the deepCamelize function:

// Transforms all keys from snake_case to camelCase:

const apiPayload = {
  chapter_title: "Loops with Mapped Types",
  author: { full_name: "Gabriel Vergnaud" },
};

const camelized = deepCamelize(apiPayload);
//    ^?
// { chapterTitle: string; author: { fullName: string } }

💡 Press the "Edit" button at the top-right corner of any code block to see type-checking in action.

Or the update function from lodash:

// Transforms a deeply nested value:

const player = { position: { x: "1", y: 0 } };
//              This is a string 👆

const newPlayer = update(player, "position.x", toNumber);
//    ^? { position: { x: number, y: number } }
//       Now it's a number! 👆

const pkg = { name: "such-wow", releases: [{ version: 1 }] };

const newPkg = update(pkg, "releases[0].version", (v) => `${v}.0.0`);
//    ^? { name: string, releases: { version: string }[] }

And any other generic function traversing nested objects. Lastly, we will see how Mapped Types can be applied to more than just object types 🤫

Let's get started!

type SnakeToCamel<Str> = Str extends `${infer First}_${infer Rest}`
  ? //    Split on underscores 👆
    `${First}${SnakeToCamel<Capitalize<Rest>>}`
  : //      Capitalize each word 👆
    Str;

type CamelizeDeep<T> =
  | {
      [K in keyof T as SnakeToCamel<K>]: CamelizeDeep<T[K]>;
    }
  | never;

declare function deepCamelize<T>(obj: T): CamelizeDeep<T>;
declare function toNumber(x: unknown): number;

declare function update<Obj, const Path, T>(
  obj: Obj,
  path: Path,
  updater: (value: Get<Obj, Path>) => T
): SetDeep<Obj, Path, T>;

type SetDeep<Obj, Path, T> =  RecursiveSet<Obj, ParsePath<Path>, T>;

type Get<Obj, Path> = RecursiveGet<Obj, ParsePath<Path>>;

type ParsePath<
  Path,
  Output extends string[] = [],
  CurrentChunk extends string = ""
> = Path extends number
  ? [`${Path}`]
  : Path extends `${infer First}${infer Rest}`
  ? First extends "." | "[" | "]"
    ? ParsePath<
        Rest,
        [...Output, ...(CurrentChunk extends "" ? [] : [CurrentChunk])],
        ""
      >
    : ParsePath<Rest, Output, `${CurrentChunk}${First}`>
  : [...Output, ...(CurrentChunk extends "" ? [] : [CurrentChunk])];

type RecursiveGet<Obj, PathList> = Obj extends any
  ? PathList extends [infer First, ...infer Rest]
    ? First extends keyof Obj
      ? RecursiveGet<Obj[First], Rest>
      : [First, Obj] extends [`${number}` | "number", any[]]
      ? RecursiveGet<Extract<Obj, any[]>[number], Rest>
      : undefined
    : Obj
  : never;

type RecursiveSet<Obj, PathList, T> = Obj extends any
  ? PathList extends [infer First, ...infer Rest]
    ? First extends keyof Obj
      ? {
          [K in keyof Obj]: Equal<First, K> extends true
            ? RecursiveSet<Obj[K], Rest, T>
            : Obj[K];
        }
      : [First, Obj] extends [`${number}`, any[]]
      ? RecursiveSet<Extract<Obj, any[]>[number], Rest, T>[]
      : undefined
    : T
  : never;

Deciphering Mapped Types

Mapped Types are often depicted as a syntax to map over each value of an object type. The name "Mapped Type" directly comes from there, and I must admit this is probably their most common use case. At their core though, Mapped Types are much simpler. They only turn unions into objects!

Support Type-Level TypeScript!

Become an Early-Bird Member to support the writing of Type-Level TypeScript and get access to all upcoming chapters!

You will find everything you need to become a TypeScript Expert — 11 chapters of in-depth, unique content, and more than 70 fun challenges to practice your new skills.

  • Full access to all 11 chapters

  • 70 type challenges with explanations

  • Lifetime access to all course materials

  • Exclusive discord community

Loading...

⟸ Previous

8. The Union Type Multiverse

Next ⟹

10. Assignability In Depth

coming soon