No Time Dad

A blog about web development written by a busy dad

An Intro to Using TypeScript Mapped Types

I’ve been digging into TypeScript’s Creating Types from Types documentation lately in an effort to try and get more comfortable with advanced types. And honestly, it’s tough. Even as someone who has been using TypeScript for a while now I struggle to understand some of the concepts with generics, operators, mapped types, and conditional types. Actually, especially conditional types.

The hard part for me is reading about the advanced type patterns and then applying them to real world scenario in code. I keep reading about patterns that sound amazing, but then putting them into action has proven difficult. I think part of the struggle is that TypeScript provides excellent types of out the box.

I find that each time I want to create my own advanced custom utility type I stumble on an existing TypeScript utility type that already does what I need. Which is interesting because the TypeScript docs even mention this in the Type cheatsheet.

These features are great for building libraries, describing existing JavaScript code and you may find you rarely reach for them in mostly TypeScript applications.

And I’ve found that to be exactly the case. I’m always working on a project that’s a TypeScript application. But I do think there’s huge value in learning more about creating custom advanced types. Which is what this post is about. Specifically, TypeScript mapped types.

Mapped types overview

I think it’s the iterator syntax in TypeScript mapped types that keeps tripping me up. It reminds me of Python list comprehensions, but it feels weird because it’s JavaScript.

type SillyType<T> = {
  [P in keyof T]: T[P]
}

The basic syntax for mapping properties looking something like the above type I’ve called SillyType because it just returns the original type, as I’ll show later. This custom type accepts any type passed to it. This T is also called a generic. It also doesn’t have to be a T, either. It can be any name like Cat or Pizza. The use of T is just a common convention, and one that’s used in other languages for generics too.

The mapping (or iteration) part of the type comes with the in keyof syntax inside the curly braces. The first thing to know is that the keyof keyword allows access to all of the properties of the generic type T. It can be thought of as taking all of the properties of type T and putting them into an array to be looped over.

type User = {
  id: number
  name: string
  active: boolean
}

So, if I passed the User type shown above to SillyType like SillyType<User>, I’d be looping over ["id", "name", "active"]. The P in the mapping represents the property, which I can modify as needed. The value for each property can also be changed as needed, which is what the T[P] in this example is doing. Here it’s just setting the value of the property to what it already was. Which is mostly useless in application, but helps illustrate how mapped types work.

type User = {
  id: number
  name: string
  active: boolean
}

type SillyType<T> = {
  [P in keyof T]: T[P]
}

type SameUserType = SameType<User>
/** 
Creates a copy of the User type
type SameUsersType = {
  id: number;
  name: string;
  active: boolean;
}
*/

Setter example

A practical example of using a mapped type would be to create a custom type for creating setter functions based on another type. For example, the User type below would be used to create a new type that includes a setter function for each property and value in User.

The UserSetters type can then be used by a class to implement each of the setter functions based on the User type.

type User = {
  id: number
  name: string
  active: boolean
}

type UserSetters = Setters<User>
/**
Should have the following definition:
type UserSetters = {
  setId: (value: number) => void;
  setName: (value: string) => void;
  setActive: (value: boolean) => void;
}
*/

The implementation of the utility type Setters is shown below. It’s looping over each property in the type passed to it and using the as keyword to restructure the property value via string interpolation. Since a setter function needs a value to set, I pass the original property type value from the User type with an argument named value.

Note that the new set functions in the UserSetters type each take a value with a type that corresponds to the origin User type definition. A number for id on setId, a string for name on setName, and boolean for active on setActive.

type Setters<T> = {
  [P in keyof T as `set${Capitalize<string & P>}`]: (value: T[P]) => void
}

type User = {
  id: number
  name: string
  active: boolean
}

type UserSetters = Setters<User>
/* 
type UserSetters = {
  setId: (value: number) => void;
  setName: (value: string) => void;
  setActive: (value: boolean) => void;
}
**/

Using the new Type

As I mentioned at the start, creating new utility types is great but actually knowing how to implement them is even better. Using the new UserSetters type as an example, it can be implemented on a class as shown below.

class User implements UserSetters {}

And right away I can see there is an issue:

Class 'Users' incorrectly implements interface 'Setters<User>'.
  Type 'Users' is missing the following properties from type 'Setters<User>': setId, setName, setActive ts(2420)

Which is actually what I want to happen. The TypeScript compiler is telling me that I’m missing properties and I need to implement them. Vanilla JavaScript isn’t going to do this. If I forget to implement a property in JavaScript I get a bug. In TypeScript that bug never reaches my transpiled JavaScript code because the TypeScript compiler won’t run with an error like this. In fact, my editor, VSCode, caught this error. I didn’t even run the TypeScript compiler.

So, I’ll start implementing each setter property with its value argument based on the definition of UserSetters. If I accidentally put the wrong type for one of the value arguments, the TypeScript compiler or my editor would tell me about it.

class Users implements UserSetters {
  setId = (value: number) => console.log("id")
  setName = (value: string) => console.log("name")
  setActive = (value: boolean) => console.log("active")
}

Wrapping up

Still much to learn about custom types and their practical use cases. Honestly, I feel like just coming up with little scenarios like “create a type that maps setters based on another type” is good way to better understand custom types, especially mapped types.

For general practice with TypeScript I’ve been using TypeScript exercises. The problems there do get more challenging as they g on. There is also type-challenges, but I do feel that some of the solutions there get wild pretty quickly. Even some of the “easy” problem solutions have me scratching my head. I’d hate to come across one of the more complex ones in an actual production application, but I’m sure they’re out there.