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 Quiz
  • 11. Designing Type-Safe APIs
  • 12. Conclusion

Articles

Subscribe

About

Made with ❤️ by @GabrielVergnaud

|Chapters|Articles
export 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;
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;

export declare function deepCamelize<T>(obj: T): CamelizeDeep<T>;

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:

import { deepCamelize } from "~/helpers/deep-camelize";

// 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);
//    camelized: { chapterTitle: string; author: { fullName: string } }
import { deepCamelize } from "~/helpers/deep-camelize";

// 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);
//    camelized: { chapterTitle: string; author: { fullName: string } }

Or the update function from lodash, which transforms a deeply nested value:

import { update } from "~/helpers/update";

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

const newPlayer = update(player, "position.x", (x) => parseInt(x));
/*    newPlayer: { 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`);
//    newPkg: { name: string, releases: { version: string }[] }
import { update } from "~/helpers/update";

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

const newPlayer = update(player, "position.x", (x) => parseInt(x));
/*    newPlayer: { 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`);
//    newPkg: { 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!

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!

Enroll in Type-Level TypeScript!

Get access to all chapters of the Type-Level TypeScript course and join a community of 1600+ students!

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

  • Full access to all 12 chapters

  • 70 type challenges with explanations

  • Lifetime access to all course materials

  • Exclusive discord community

Loading...

⟸ Previous

8. The Union Type Multiverse

Next ⟹

10. TypeScript Assignability Quiz