From 7e5e14229e17bdacc3045ac01c91a0b655541254 Mon Sep 17 00:00:00 2001 From: Christian van der Loo Date: Fri, 30 May 2025 15:14:01 -0400 Subject: [PATCH] improve contents --- src/content/blog/ts-mapped-types.md | 49 +++++++++++++++++++---------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/src/content/blog/ts-mapped-types.md b/src/content/blog/ts-mapped-types.md index 5e88486..cff5433 100644 --- a/src/content/blog/ts-mapped-types.md +++ b/src/content/blog/ts-mapped-types.md @@ -1,8 +1,8 @@ --- -title: Maintaining Context in TypeScript with Mapped Types +title: TypeScript Wizardry with Mapped Types date: 2025-04-20 -slug: using-typescript-mapped-types -description: How to use complex mapped types with inferred generics. +slug: typescript-wizardry-mapped-types +description: A brief dive into TypeScript's ability to represent complex types using mapped types, generics, and type inference. tags: [typescript] --- @@ -65,7 +65,7 @@ In the case of our box example, that `clone` declaration seems like it will be t function createBox(contents: string): Box { const innerBox = { contents, - clone: () => ({ ...box }) + clone: () => ({ ...innerBox }) } return innerBox } @@ -79,7 +79,7 @@ Okay, let’s reintroduce generics. The function itself will need to have a gene function createBox(contents: T): Box { const innerBox = { contents, - clone: () => ({ ...box }) + clone: () => ({ ...innerBox }) } return innerBox } @@ -150,7 +150,8 @@ TypeScript has correctly inferred the input arguments to match our `MyFuncs` typ Let’s now map the return types to Promises. We can use the built-in `Parameters` and `ReturnType` utility types to extract the parameters and return type of each function. Then, we just need to reconstruct the function, with a `Promise` around the return type. To do this on one function, it might look like this: ```ts -type ToAsync any> = (...args: Parameters) => Promise> +type ToAsync any> = + (...args: Parameters) => Promise> type Result = ToAsync<(arg: string) => string> // ^ type Result = (arg: string) => Promise @@ -160,11 +161,12 @@ Okay, now let’s apply `ToAsync` in a mapped type. ```ts type MyFuncs = { - a: (arg: string) => number; - b: (arg: Box) => Box; + a: (arg: string) => number + b: (arg: Box) => Box } -type ToAsync any> = (...args: Parameters) => Promise> +type ToAsync any> = + (...args: Parameters) => Promise> type MyFuncsAsync = { [K in keyof MyFuncs]: ToAsync @@ -179,8 +181,12 @@ type MyFuncsAsync = { The statement `[K in keyof MyFuncs]` gives us a new type `K` that we can use to map into `MyFuncs`. The statement lets us use each field in `MyFuncs` as though it were a new generic argument, and TypeScript will take those results and reconstruct a new mapped type. Here’s the above, resulting in `MyFuncsAsync` again, but this time using a generic utility type: ```ts -type ToAsync any> = (...args: Parameters) => Promise> -type MappedToAsync = { [K in keyof T]: ToAsync } +type ToAsync any> = + (...args: Parameters) => Promise> + +type MappedToAsync = { + [K in keyof T]: ToAsync +} type MyFuncsAsync = MappedToAsync ``` @@ -188,7 +194,9 @@ type MyFuncsAsync = MappedToAsync With this new utility type, we can finish out the signature of the `makeArgsAsync` function: ```ts -function makeArgsAsync(args: T): MappedToAsync { +function makeArgsAsync( + args: T +): MappedToAsync { // todo } @@ -208,7 +216,9 @@ Success! We’ve now created a mapped type that maps an object of synchronous fu The part that many resources leave out is actually doing the implementation while maintaining type-safety. One recommendation I have: process each argument in a separate function, with its own generics and return. This way, you can mentally keep the focus on the constraints of the specific problem. I wouldn’t recommend putting all the implementation inside of a for loop or mapping inside the main function. Here’s an implementation of just making one function asynchronous: ```ts -function makeArgAsync any>(fn: Fn): ToAsync { +function makeArgAsync any>( + fn: Fn +): ToAsync { return async (...args) => { return new Promise>((resolve) => { setTimeout(() => { @@ -222,9 +232,14 @@ function makeArgAsync any>(fn: Fn): ToAsync { Now, let’s use it. ```ts -function makeArgsAsync(args: T): MappedToAsync { +function makeArgsAsync( + args: T +): MappedToAsync { return Object.fromEntries( - Object.keys(args).map((key) => [key, makeArgAsync(args[key])]), + Object.keys(args).map((key) => [ + key, + makeArgAsync(args[key]) + ]), ) as MappedToAsync } ``` @@ -235,9 +250,9 @@ Additionally, `Object.keys` does not return the type `(keyof T)[]`. Instead, it ```ts type ObjType = { a: number } -const obj: ObjType = { a: number; b: number } +const obj: ObjType = { a: number, b: number } ``` `obj` satisfies the structural type of `ObjType`, but the key `b` will show up during runtime, hence the difference in type for `Object.keys`. -Using `as` is unavoidable when working with complex mapped types for the above reasons. As the developer, you can safely assert that all keys will be operated on once, and you have to inform TypeScript of that information. To keep things safe, I recommend that the functions operating on the entire mapped type be as small as possible, and defer to smaller functions that operate on each field. It lets you maintain focus on the explicit generics you care about in the moment. \ No newline at end of file +Using `as` is unavoidable when working with complex mapped types for the above reasons. As the developer, you can safely assert that all keys will be operated on once, and you have to inform TypeScript of that information. To keep things safe, I recommend that the functions operating on the entire mapped type be as small as possible, and defer to smaller functions that operate on each field. It lets you maintain focus on the explicit generics you care about in the moment. -- 2.51.2