DevJoseManuel
DevJoseManuel

DevJoseManuel

TypeScript Challenge - #02 - Readonly

TypeScript Challenge - #02 - Readonly

DevJoseManuel's photo
DevJoseManuel
·Aug 6, 2022·

6 min read


In this challenge what we will pursue is to build the Readonly<T> utility offered by TypeScript but without using it, which implies that what we are going to build is a type that will return all the attributes of type T that have been set as readonly, which means that these attributes cannot be reassigned (it will not be possible to change their value).

As an example:

interface Todo {
    title: string
    description: string
}

const todo: MyReadonly<Todo> = {
    title: 'Hey',
    description: 'foobar'
}

todo.title = 'Hello' // Error: cannot reassign a readonly property.
todo.description = 'barFoo' // Error: cannot reassign a readonly property.

Solution

type MyReadonly<T> = {
    readonly [K in keyof T]: T[K]
}

// If we want to remove de readonly we can do
type MyReadonly<T> = {
    -readonly [K in keyof T]: T[K]
}

Explanation

The first thing we have to think about in order to build our solution is that the readonly visibility modifier assignment should only be applied on those first level attributes of the data type on which our utility will work. What does this mean? Well, if we extend the All interface declaration we have in the example:

interface Todo {
    title: string
    description: string
    completed: boolean
    meta: {
        author: string
    }
}

the readonly modifier should only be applied to the first level attributes (title, description, completed and meta) but not to the second level and later ones (which means that the author attribute of the meta object does not have to have it). That is, what we have to obtain is a data type like the following:

interface Todo {
    readonly title: string
    readonly description: string
    readonly completed: boolean
    readonly meta: {
        author: string
    }
}

To obtain the solution the first thing we have to think is that somehow what we are being asked is that our utility creates a new object from a starting one, so whenever we face a challenge of this type the best idea to face it is to think of a mapping of the properties of the starting object in the properties of the object with the solution.

So as an initial step we establish that the result of applying our type is going to be a new object:

type MyReadonly<T> = {}

And how can we go through all the properties that are collected in the T object? Well, first of all we will have to obtain them, which we achieve thanks to the use of the keyof operator on the object T. In the example we are working with we will have something like the following:

keyof Todo --> 'title' | 'description' | 'completed' | 'meta'

and now we are going to have to traverse them as if we were traversing a JavaScript array for which we will rely on the in operator as follows:

K in keyof Todo --> ['title', 'description', 'completed', 'meta']

Now that we know how to obtain all the attributes of the object T the next thing we have to do is to create a mapped type, that is to say, a type that is obtained by mapping the data of another type. In our case what we want is to create a new object where each of its attributes is the same as the starting T object so we would write something like the following:

type MyReadonly<T> = {
    [K in keyof T]: ...
}

And what value will we assign to each of these attributes? Well, in principle the same as the original object, which we obtain by accessing directly to that attribute in T:

type MyReadonly<T> = {
    [K in keyof T]: T[K]
}

What have we achieved so far? Well, the truth is that very little since the only thing that the previous declaration is doing is to return us exactly the same type T with which we are working.

We will have to add the modifier readonly to be able to achieve the objective that we are pursuing for which we will have several possibilities. The first of them would consist of adding it to the part that collects the value of the new data type:

type MyReadonly<T> = {
    [K in keyof T]: readonly T[K]
}

but here the problem lies in that TypeScript will give us an error because readonly is a modifier that can only be applied on arrays or tuples and we do not have the certainty that T[K] is going to be an array or tuple.

The other possibility we have is to add it before the declaration of each of the attributes of our new object:

type MyReadonly<T> = {
    readonly [K in keyof T]: T[K]
}

And with this we would achieve the result we are looking for.

Remove the readonly attribute

Now that we know how to add the readonly attribute, just mention that TypeScript offers us the possibility of using the - operator in front of this modify and what it will do is to remove it from all those attributes that may have it in the T object.

type MyReadonly<T> = {
    -readonly [K in keyof T]: T[K]
}

An additional level of depth

We could go one level deeper in the declaration of the attributes on which the readonly modifier has to be removed, being this an interesting aspect to which it is worth dedicating a few moments.

The first step to achieve this is to use a conditional type to determine whether the value assigned to the attribute we are working with is an object or not:

type MyReadonly<T> = {
    readonly [K in keyof T]: T[K] extends object ? ... : ...
}

And here is where it comes into play to know that TypeScript will allow us to make recursive to the data types that we are defining in an analogous way to how recursive function calls are made in JavaScript. What does this mean? Well, in our case, if the condition is fulfilled, what we will do is to apply MyReadonly again, but in this case passing the object that is associated to the K attribute of the starting T object:

type MyReadonly<T> = {
    readonly [K in keyof T]: T[K] extends object
        ? MyReadonly<T[K]>
        : ...
}

and in the case that the condition is not fulfilled (that is to say, that the value that has assigned the attribute K is not an object) what we will return will be this value:

type MyReadonly<T> = {
    readonly [K in keyof T]: T[K] extends object
        ? MyReadonly<T[K]>
        : T[K]
}

 
Share this