Deeply mark all the properties of a type as read-only in TypeScript

Rares Matei
InstructorRares Matei

Share this video with your friends

Send Tweet
Published 4 years ago
Updated 4 years ago

We will look at how we can use mapped types, conditional types, self-referencing types and the “infer” keyword to create a reusable generic type that traverses down the properties of an object and marks of all them as read-only. This is especially useful when used with Redux, to ensure our whole state tree is marked as read-only and immutable.

Instructor: [00:00] Here I have a Redux reducer. Above here, I have the actual definition of the state. As you can see, it's very deeply nested. We have a few root level properties, but then in that, we have an array of todos, which is another complex object which actually has a link to another object.

[00:16] In Redux, the state needs to be immutable. If there's an existing instance of the state, I should not be able to reassign any of its properties, or any of the properties of any of its substates, regardless of how deep down the tree we go. This includes not being able to reassign array values at specific indexes, or even the properties of objects at certain indexes in the array.

[00:42] You can see that everything works fine in this case. One thing we could do to prevent this is we could start adding the read-only flags throughout the state, and mark all arrays as read-only arrays. Ideally, we want to avoid that. We want to define the state shape with as little overhead as possible. Only when it's applied in the context of a reducer, it needs to be read-only.

[01:07] If we had some automated way to get from this type to this one, we could then focus on what the state actually represents, how to best structure it, and then let some automated system add all the read-only flags for us before it's actually applied to the reducer.

[01:24] By doing that, we also ensure that whenever developers start changing the shape of the state, they don't forget to add read-only flags, because they'll be added automatically.

[01:33] How do we actually transform one type into another? The answer to that is generics. I'll call my transformer deep read-only, and I'll first use map types to loop over every key of the type that is passed into here. For each key, I'll return the original type of its value, but then I'll add a read-only flag to it.

[01:53] If I scroll down here and I replace the type of my state with a read-only state, I can see that my change actually took care of the first few properties in here. You might be tempted to just recursively apply this type again to each property, which will surely make it read-only even at the deeper levels. If we hover over it, we can see that it's complaining about it.

[02:14] That might seem like it solved our problem, but it's also going to start messing with some of the types. For example, it won't let me map over arrays now, even though map is non-mutative.

[02:25] We want to be careful and specific about how we mark each type as read-only. Let me rename this one as just for objects. Then I'll create a new one where I can start to be specific about how I transform each type. I'll call this one deep read-only, and it will actually be the starting point.

[02:43] It's first going to check, is my type an array? If so, infer the type of its elements and return a read-only array instead by applying the deep read-only object recursively on each of its wrapped types. Otherwise, if it extends an object, apply the deep read-only object type directly. Finally, if it's none of those, it's a flat primitive, so just return T.

[03:07] You'll notice the recursive loop here. I start with a deep read-only type. A title returns D if it's a primitive and attends, or it calls the deep read-only object type if it's an object or an array. The deep read-only object type goes through each key and, in turn, it calls the deep read-only type again with the type of that property.

[03:27] This recursive loop is what's going to drill down the properties of objects and arrays, ensuring everything is read-only, regardless of depth. If I go back to my assignments here, I can see that it's complaining correctly about all of them. If I hover over them, I get a really useful message.

[03:43] It's also not breaking the arrays, and it's letting me map over them, including correctly inferring what type each of their elements is.

[03:51] Before we wrap up, let's go back to this statement over here, as it's a bit suspicious. It works great if T is an array of objects, as in the case of my todos array, because we're transforming an array of objects into a read-only array of read-only objects.

[04:07] What if we have an array of primitive strengths, like we have for this property over here? How would that work? It turns out that using mapped type modifiers with primitives just returns the primitive itself, because there's no key to map over. You can also see that down here. That's why it lets me call to upper case on each todo.

[04:27] It works for arrays of primitives and arrays of objects, but what if we have an array of arrays, like I have here? That will indeed take us to our previous problem and break the inner array, as that's the one we're sending to the object mapper. We won't be able to use array methods on it.

[04:45] If you want to be extra careful and make it work, we can add another line here that checks for arrays of arrays, and converts it to a read-only array of read-only arrays. That's going to complicate matters a bit, so I'd be careful about adding this unless you really think you need it. If we look down here now, we can see that our mapping works correctly.