Statically Type String Literals with Template Literal Types in TypeScript

Marius Schulz
InstructorMarius Schulz
Share this video with your friends

Social Share Links

Send Tweet
Published 4 years ago
Updated 4 years ago

In this lesson, we're going to explore template literal types, another powerful feature of TypeScript's type system. Template literal types have the same syntax as template literals in JavaScript, but they're used in type positions. Using template literal types, we can produce a union of string literal types and perform string concatenation in the type space:

type Dimension = "block" | "inline";
type MarginProperty = `margin-${Dimension}`;

Additional Reading

Instructor: [0:00] Let's say that we wanted to start a key type CSS declarations in typescript. Here we have a union type of the two string literal types, margin block and margin inline. We could have also written this type declaration using typescript's template literal types.

[0:16] First, let's extract the type called dimension that's equals to the string literal type block, or the string literal type inline. Now, we can rewrite this union type using a template, literal type.

[0:29] To define the template literal type, we use the same backtick syntax that we use for template literals in JavaScript. We want our template literal type to begin with the prefix, margin hyphen. Then we want to interpolate our dimension union type.

[0:44] Just like with the backtick syntax, we're using the same syntax for interpolating values. The dollar sign followed by curly braces. Dimension is a union type so typescript will expand our template literal type into a union of all the strings that we can represent this way.

[1:01] If I hover over the type name, you can see that the generated string literal types are, margin block and margin inline. Template literal types let us interpolate more than one literal type.

[1:14] Let's assume we also had to find a type called direction, that could either be the string literal type start or the string literal type end. If I go back into our template literal type, we can now also interpolate the direction type.

[1:30] Our imagine property type now represents four string literals. The combinations of all values of dimension and direction. We get margin block start and margin block end, as well as margin inline start and margin inline end.

[1:45] Let's keep going with this example for a bit and let's see how far we can take it. Let's say that we also wanted to define a margin value type that would be equal to a number. We're simply creating a type alias here.

[1:58] Using our margin property and margin value types we can now create a margin declaration type. For the sake of simplicity, I've decided to go with a tubal type here, but you could have also modeled this as an object type.

[2:12] Go ahead and actually create imagine value. I'm going to say const imagine, and I'm going to add a type annotation here, equals, as I type the double quotes, you can see that typescript gives us autocompletion suggestions.

[2:27] For example, I can pick margin block start, and we can set a margin block start value of 16. Note that all of this is fully type checked. If we make a typo here, typescript gives us a type error and it's saying that our incorrectly spelled property name is not assignable to this union type.

[2:49] Notice that we're using a number here. Let's say we wanted to make this a string value with the pixel suffix here. We can statically type this using another template literal type. This time though, we'll be interpolating the type number.

[3:04] If I come back down to our margin value, you can see that we now get a type error if we use a unit other than pixel, or if we leave out the unit entirely. Let's now say that we wanted to add support for other units, for example, the viewport height unit.

[3:20] In our template literal type, we can add another union type. In here we can say that we either accept the unit pixels, or the unit viewport height, or the unit viewport width. If this union type gets too unwieldy, we can always extract it into a named type, for example, margin unit.

[3:43] Before we wrap up this example, I want to simplify the code. Our margin property type is fully known ahead of time. There's no need for us to use the template literal type to put it together dynamically. We can simplify this code and write out the entire union type ourselves.

[4:09] 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. 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.

[4:28] If I write all of this to the console, we can see what that looks like. Let's call that get-name method, and let's also call the get-Twitter method. You can see that the resulting object has two methods getName and getTwitter.

[4:48] When we call those, we get back the original values. 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.

[5:04] We're also going to add a generic type parameter to our function. We're going to update the types of our parameters as well as the return type, now we're getting a type error in line six. This is because our generic type parameter is currently unconstrained. It could actually be any type we can think of. We cannot safely access into it like this.

[5:27] 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.

[5:42] 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. 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.

[6:03] There's a lot going on in this line, so let's have a detailed look before we continue. 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. Then we map all of these property names to a function type. The property name is represented by the T key type.

[6:27] 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 index access up type. Essentially, we're looking up the type of the property named T key in T object.

[6:47] For example, for our name property in this object the resulting type would be a string. 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, both of them are functions returning as string.

[7:04] Notice that the names of the properties aren't quite right yet. 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. 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.

[7:34] Typescript isn't quite happy with us yet. It's giving us a big pyramid of an error message. 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.

[7:49] Given that we only care about string properties in this example, we can add an intersection type here. 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.

[8:07] Now that we have the key remapping in place, let's go down to line 20 and let's see how far we got. You can see that we now get two properties, getName and getTwitter. However, notice that the capitalization isn't quite right yet.

[8:21] 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. The capitalized helper type takes the string literal type and it upper cases the first character. Let's have a look at its definition.

[8:40] You can see that it's defined using the intrinsic keyword. The intrinsic keyword is reserved for a special set of helper types that are implemented in the typescript's compiler itself. We couldn't write this capitalized type ourselves.

[8:55] There are a bunch of other types that are defined the same way. 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. Let's hit the dot.

[9:14] 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. 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 and you can see that the property has the expected type, a function returning a number.

[9:41] 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.

[9:52] Let's 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. They let you statically type even highly dynamic code, and you can get even more advanced than what I've shown you here.

[10:13] I would encourage you to take some time and play around with all of these advanced types to see how far you can push the type system.