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 — 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...