DevJoseManuel
DevJoseManuel

DevJoseManuel

TypeScript Challenge - #01 - Pick

TypeScript Challenge - #01 - Pick

DevJoseManuel's photo
DevJoseManuel
·Jul 30, 2022·

7 min read


In this challenge what we will pursue is to construct the Pick<T, K> type without making use of it. We will have to construct a data type that will allow us to extract the set of K properties of a T type. For example:

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

type TodoPreview = MyPick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
    title: 'Clean room',
    completed: false
}

Solution

MyPick<T, K extends keyof T> = {
    [P in K]: T[P]
}

Explanation

We will start from the basic definition of the data type we want to construct where we will set the parameters we expect it to receive and we will also assume that it will initially return any:

type MyPick<T, K> = any

But before we continue, let's stop for a moment to understand how MyPick should work. To do so, we are going to rely on the declaration of the All interface of the challenge definition by presenting three scenarios:

MyPick<Todo, 'title'>
MyPick<Todo, 'title' | 'completed'>
MyPick<Todo, 'title' | 'completed' | 'invalid'>

How should MyPick behave in each of the three scenarios above?

  • In the first case MyPick should not give an error since title is one of the attributes that are collected inside the All interface.
  • In the second case we should not have an error either since the union type that is formed by title and completed corresponds to two of the attributes that are defined in MyPick.
  • The third case is different since we are again in front of a union type but in this case the particularity is that one of the values that form it does not correspond to one of the attributes of the All interface so MyPick should inform us about it with an error message.

With this in mind it seems that the key to solving our problem is to somehow restrict the set of values that we can write to the second parameter of MyPick<T, K> or, in other words, to define K in such a way that it can only accept values that correspond to the attributes of T.

keyof

Now how can we say in TypeScript that we want to extract the set of attributes that are associated with a type? The answer is thanks to the use of the keyof operator.

To understand it better with our example, if we apply this operator to the interface All what we are going to obtain is a union type formed by the name of all the attributes that are defined inside the interface.

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

extends

Now that we have the data type formed by all the attributes of the interface we have to restrict in some way that only one of them can be chosen and this is where the conditional types come into play.

Thinking in terms of set theory:

  • What do we want to prove? That the set formed by the data types that is associated to all the attributes we want to extract is a subset of the set formed by the type attributes we are working with.
  • Does TypeScript offer us a mechanism to be able to perform this check? Yes, and it is the use of the conditional types or, in our case, of the extends operator.

As we can guess extends is an operator that will allow us to perform checks in a similar way as we do JavaScript:

A extends B ---> true whenever A is a subset of the elements of B.
A extends B ---> false otherwise.

Let's translate this idea into the scenarios we have shown above:

B = keyof Todo ---> 'title' | 'description' | 'completed'

'title' extends B --> true 
'title' | 'completed' extends B --> true
'title' | 'completed' | 'invalid' extends B --> false

Putting all the pieces together

Now we only have to join the two previous pieces to achieve our goal by establishing that the set of values that can be set in the K parameter must belong to the set of attributes that is collected in the T type. This leaves us with the following:

MyPick<T, K extends keyof T> = any

Returning the value

In the previous statement we are seeing that the result that is being returned is any and this is not what is intended in the challenge since what we want is to return an object that contains only the attributes of T that we have specified in the K parameter.

The way to achieve this is by using what is known as a Mapped Type which is nothing more than constructing a new type from the definition of another.

Let's start by defining the simplest mapped type that we can think of to achieve our goal, which would be one that always returns a type without attributes as a result:

MyPick<T, K extends keyof T> = {}

How do we now define the attributes of this object? Well, this is where we have to understand that both T and K are two types that will be available during the declaration of the type we are constructing (to some extent, if we think in terms of JavaScript, it is as if both types were linked to the scope of the declaration of our new type) and that, in addition, for the definition of the attributes we will be interested in the set that is contained in K.

Is there a possibility to traverse all the values collected in K in TypeScript? The answer is yes, and it is thanks to the use of the in operator. Thinking in terms of arrays we could make a mental scheme like the following:

K ---> 'title' | 'description' | 'completed'
P in K ---> ['title', 'description', 'completed']

where P is a type that will pass the title, description and completed values respectively while traversing the array.

And can we define an attribute within the mapped type from each of these array elements? The answer again is yes, using the notation between [] where

MyPick<T, K extends keyof T> = {
    [P in K]: ...
}

This, which is complex to explain in words, is best seen in the example we are following:

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

MyPick<Todo, 'title'> ---> { title: ... }
MyPick<Todo, 'title' | 'description'> ---> { title: ..., description: ... }
MyPick<Todo, 'title' | 'description' | 'completed'> ---> { title: ..., description: ..., completed: ... }

Now we only have to assign the data type that is associated to each of the attributes that we are extracting and this is achieved in a very simple way just using the operator [] on the data type indicating which is the element from which we want to extract the information of the type (analogously to how we can do it in TypeScript to access each of the elements of an object).

But how do we know which is the concrete attribute? Well, we have to remember again that all the generic types that we use during the declaration of our type can be used in the rest of its definition.

In our case, as we have declared something like [P in K] to go through all the attributes that are collected within the set of keys to extract K and as we also have the certainty that these keys will belong to the type on which we are working T we can simply use T[P] to obtain the type associated with that attribute:

MyPick<T, K extends keyof T> = {
    [P in K]: T[P]
}

If we go back to our example we would get something like the following:

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

MyPick<Todo, 'title'> 
    ---> { title: string }
MyPick<Todo, 'title' | 'description'> 
    ---> { title: string, description: string }
MyPick<Todo, 'title' | 'description' | 'completed'> 
    ---> { title: string, description: string, completed: boolean }

 
Share this