Use TypeScript's Mapped Types and Template Literal Types Together

Marius Schulz
InstructorMarius Schulz

Share this video with your friends

Send Tweet
Published a year ago
Updated a year ago

In this final lesson, we're going to dive deep into a more complex example in which we combine mapped types, key remapping, template literal types, and indexed access types to statically type a highly dynamic JavaScript function in TypeScript.

Instructor: [0:00] I want to show you how we can use template literal types in a more complex example. I've prepared a function called, create getter object.

[0:08] What that function does is it accepts an object like this user object here and it returns a new object in which all the properties have been replaced by getter functions. If I write all of this to the console, we can see what that looks like.

[0:24] Let's call the getName method and let's also call the getTwitter method. You can see that the resulting object has two methods, getName and getTwitter and when we call those we get back the original values.

[0:45] Let's try and statically describe this behavior in typescript. I'm going to create an object type called prop getters with a single type parameter T object. We're also going to add a generic type parameter to our function.

[0:59] We're going to update the types of our parameters as well as the return type. We're getting a type error in line six and this is because our generic type parameter is currently unconstrained. It could actually be any type we can think of.

[1:16] We cannot safely access into it like this. What we'll need to do is to add a constraint to our generic type parameter. We only want this function to be used with object types. Those object types have to have string keys, but they can have arbitrary values.

[1:33] The first thing we want to do in our prop getters type is to make it a mapped type. A mapped type let's us create a new object type from an existing object type. This is exactly what we need here.

[1:45] We want to map over all keys in the object type and we want the resulting type to be a function type that returns the type of that property. There's a lot going on in this line so let's have a detailed look before we continue.

[2:00] We're using the key off operator here to get all keys of this object type. The key off operator produces a union type of all the property names and then we map all of these property names to a function type.

[2:15] The property name is represented by the T-key type. We can refer to it by using T-key. Here, we're writing out the function type. It doesn't take any parameters and the return value is a look-up type. Also known as an indexed access type.

[2:33] Essentially, we're looking up the type of the property named T-key in T-object. For example, for a name property in this object, the resulting type would be a string.

[2:45] If I go down to line 20 and I hit the dot here, you can see that typescript suggests two properties in the autocompletion list and both of them are functions returning as string. Notice that the names of the properties aren't quite right yet.

[3:00] The names are currently Name and Twitter but we want them to be getName and getTwitter. This is where we can use a template literal type. We can come back into our map type and we can add a key remapping.

[3:14] We're going to remap the keys onto a template literal type. We want our property keys to be the string, get followed by the name of the key. Typescript isn't quite happy with us yet and it's giving us a big pyramid of an error message.

[3:31] The problem is that our keys are not necessarily literal types, but we need them to be literal types so that we can interpolate them into our template literal type. Given that we only care about string properties in this example, we can add an intersection type here.

[3:46] We can say that we want the intersection of all keys in the type string. This way we'll only keep those property keys that are strings and the type error goes away. Now that we have the key remapping in place, go down to line 20 and see how far we got.

[4:05] You can see that we now get two properties getname and gettwitter. However, notice that the capitalization isn't quite right yet. This is because we're interpolating the name of the key exactly as it is. What we want to do instead is to capitalize the key name.

[4:22] The capitalized helper type takes a string literal type and it uppercases the first character. Let's have a look at its definition. You can see that it's defined using the intrinsic keyword.

[4:36] The intrinsic keyword is reserved for a special set of helper types that are implemented in the typescript compiler itself. We couldn't write this capitalized type ourselves. There are a bunch of other types that are defined the same way.

[4:50] We also have types for uppercasing strings, lowercasing strings, and uncapitalizing strings. Notice that all the type errors have gone away now. Let's go down to line 20 one more time.

[5:03] Let's hit the dot and now you can see in the autocompletion list that we have our properties getName and getTwitter and both of them have the expected function types.

[5:15] If we now add another property to our user type, for example, a numeric ID, we can come back down here and we can call user.getID. You can see that the property has the expected type. A function returning a number.

[5:33] Before we wrap up, there's one final tweak that I want to make. That is to add a type constraint to the T object type parameter. We want to make sure that people use the prop getters type correctly.

[5:44] Add the same type constraint we've added above and let's require the object types to have string keys and arbitrary values. As you've seen, template literal types can be really powerful and flexible.

[5:57] They let you statically type even highly dynamic code and you can get even more advanced than what I've shown you here. I would encourage you to take some time and play around with all of these advance types to see how far you can push the type system.

Enrico
Enrico
~ a year ago

Why do we need the "TKey in string"? Why does TypeScript say it can be 'string | number | symbol' even though we said TObj extends Record<string, unknown>? Thanks for the course :)

Andrew Ross
Andrew Ross
~ a year ago

Enrico I think those are the primitive types (string | number | symbol) sans boolean, so I think by adding the intersection type TKey in string & keyof TObj... it denotes that we are filtering(?) TKey, otherwise of types string | number | symbol | keyof TObj, on the basis of whether a corresponding key is of type string or not. All non-string keys get 86'd (discarded) by incorporating this intersection.

Please correct me if my understanding is off

Andrew Ross
Andrew Ross
~ a year ago

Actually, after thinking on it, it seems that it is actually coercing the type of TKey to string, not necessarily filtering them