Infer Types for Rest and Spread Properties in TypeScript

Marius Schulz
InstructorMarius Schulz

Share this video with your friends

Send Tweet
Published 5 years ago
Updated 3 years ago

TypeScript supports rest and spread properties for objects, which are slated for standardization in ECMAScript 2018. It automatically infers rest and spread types so that you can use object spread and rest elements in a statically typed manner without having to manually add type annotations.

Here we have a simple person object with three properties, full name, blog, and Twitter, all of type string. We can go ahead and use a destructuring pattern to deconstruct the object into its properties. We can say const full name, blog, Twitter equals person. Now, the text compiler will infer that full name is of type string, and so is blog, and so is Twitter.

Instead of destructuring all properties separately, we can also use REST properties. Let's call this variable social media and see what it looks like. Full name is a string as it was before. Social media is now typed to be an object with the two properties blog and Twitter, which is what is called a REST type.

This is really nice, because we can now access the social media properties in a type-safe manner. We can access blog and Twitter, but not full name, because full name is not part of the social media object. We've picked off the full name property in the same destructuring pattern before, so it's not going to be part of our REST property element.

Note that a REST element must be the last in a destructuring pattern. After a REST element, we cannot destructure a name property. The TypeScript compiler will complain and tell me that a REST element must be last. That was REST properties.

Let's now take a look at spread properties. Let's assume we have an object called default styles which holds some default CSS values. For example, a font family which is set to Arial and sans serif, and font weight of, let's say, normal. Now we can also have user specific styles, so we'll also say user styles. The user can now override some property values.

For instance, I can specify a specific color, and they can override the font weight. Our goal now would be to merge the two objects together. How would we do that?

One approach is to use spread properties. We could go ahead and create a new object called styles, and we could spread in all the default styles. Afterwards, we could spread in all the user styles.

Let's now check out the types that we have here. Default styles in inferred to be an object with two properties, font family and font weight, both of type string. Similarly, user styles is inferred to be an object with two properties, color and font weight, of types string and number.

Now, styles in inferred to be the union of both. We have a color property of type string which originates from the user styles object, and we have a font weight property of type number which also originates from the user styles object. Finally, we have a font family property which comes from the default styles object.

Notice that the order is important here. If we were to spread the user styles object first and the default styles, the resulting type would be different. We would still have a font family property of type string and a color property of type string, but now we also have a font weight property of type string which means that this font weight was applied, and not this one.

Also semantically, it doesn't really make sense to apply the user styles first, and override them with all the default values. The default values should be the fallback, not the other way around.

Just remember that this spread type accurately reflects what's happening here. We're first assigning all the user styles and all the default styles. Now that we have the combined styles object, we can also destructure it into its properties.

Let's say we have a destructuring pattern here, and we want to pick off the color property. That will be of type string. We can also add a rest property called remaining props. That will correctly be inferred to be of type object with two properties, font weight and font family.

Finally, I want to show you how to make shallow copies of an object using the spread property syntax. Let's say we have defined a to do object with a text such as mow the lawn, which is not completed yet and which is tagged as garden.

I can make a shallow copy of this object, let's call that shallow copy, by saying dot, dot, dot, and then to do. I'm basically creating a new object, and I'm assigning all own innumerable properties of the to do object. The type of the shallow copy variable the TypeScript infers is as we would expect. We have a text property of type string, we have a completed property of type Boolean, and we have a tags array that can hold strings.

Now, let me go ahead and log to the Console, whether the to do object is the same as the shallow copy. Let me pop open the Console and actually run the program. I'll say node source index.ts. We get the expected result false, because we have two separate objects. If I go ahead now and log out the shallow copy itself, we can see that we have the correct values.

Let's go ahead and change the text property of our shallow copy. I'll say shallow copy.text equals buy milk. What I want to do now is I want to log the shallow copy, and afterwards, I want to log the original to do to see that we haven't changed anything. Let's open up the Console again and execute the program. We can see that while we have changed the text property of the shallow copy, we have not modified the original text.

What happens if, in addition to modifying the text property, we also add another tag. What if we say tags.push and for example kitchen. Let's find out. I'll run the program again. This time we can see that our program does not behave as intended.

Note that we now have the kitchen tag here and here, in both to dos. This is because we've only made a shallow copy of the object, so when we copied over all properties of the to do object into shallow copy, we copied the reference to the same array. We've not made a deep clone.

If we push a tag into the array, it's visible in both to dos, because again, we only have one array instance. If you want to make a deep clone of an object, you should refrain from using spread properties and use a proper deep cloning approach.

Jodi Warren
Jodi Warren
~ 5 years ago

At 6:25, you execute a TypeScript file directly in Node. Have you aliased ts-node to node, or is there another handy tip which I don't know about?

Marius Schulz
Marius Schulzinstructor
~ 5 years ago

@Jodi: While the file has a .ts extension, it is still just a plain JavaScript file with no TypeScript syntax at all. Therefore, Node can run it without any problems (and doesn’t care about the file extension).

Jodi Warren
Jodi Warren
~ 5 years ago

Of course it is! Thanks Marius, that makes perfect sense.