Other languages: Spanish
Link to the challenge: Pick
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 sincetitle
is one of the attributes that are collected inside theAll
interface. - In the second case we should not have an error either since the union type that is formed by
title
andcompleted
corresponds to two of the attributes that are defined inMyPick
. - 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 soMyPick
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 }
Top comments (0)